Providers/AD/Public/Remove-ADEnvironment.ps1

function Remove-ADEnvironment {
    <#
    .SYNOPSIS
        Safely removes all AD test data created by the ADTestEnvironment module

    .DESCRIPTION
        Performs a complete cleanup of test data including users, devices, security groups,
        and optionally OUs. Includes safety checks and progress reporting.

    .PARAMETER RemoveOUs
        Also removes the test OU structure (WARNING: This is destructive)

    .PARAMETER VaultName
        Name of the SecretStore vault to remove. Defaults to "ADTestEnvironment"

    .PARAMETER Force
        Bypasses confirmation prompts (use with caution)

    .PARAMETER ResetSecretStore
        Also reset the global SecretStore configuration to defaults.
        WARNING: This will affect ALL SecretStore vaults on the system.
        Use with caution if you have other vaults configured.

    .PARAMETER GlobalVault
        Create/remove vault at AllUsers scope instead of CurrentUser scope.
        Requires administrative privileges.

    .PARAMETER PassThru
        Returns detailed results object (default: summary only)

    .PARAMETER WhatIf
        Shows what would be removed without making changes

    .EXAMPLE
        Remove-ADEnvironment -WhatIf
        Shows what would be removed without making changes

    .EXAMPLE
        Remove-ADEnvironment -RemoveOUs -Force
        Removes all test data including OUs and SecretStore vault without confirmation

    .EXAMPLE
        Remove-ADEnvironment -VaultName "CustomVault"
        Removes test data and a custom named vault

    .EXAMPLE
        Remove-ADEnvironment -ResetSecretStore -Force
        Removes test data, vault, AND resets SecretStore configuration without prompts

    .EXAMPLE
        Remove-ADEnvironment -GlobalVault -Force
        Removes test data and a global vault (requires admin privileges)

    .OUTPUTS
        Hashtable with removal results and statistics

    .NOTES
        Author: Jeffrey Stuhr
        Version: 2.0.0
        Last Updated: 2025-08-05
        
        WARNING: This function is destructive. Always test with -WhatIf first.
        
        SECRETSTORE CLEANUP:
        - Automatically removes SecretStore vaults created during environment setup
        - All stored passwords/secrets will be permanently deleted
        - Vault removal is a management operation that doesn't require the vault password
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
        Justification = 'Colour-coded console progress is intentional; results are returned as objects.')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([System.Collections.Hashtable])]
    param(
        [switch]$RemoveOUs,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$VaultName = "ADTestEnvironment",
        
        [switch]$Force,
        [switch]$PassThru,
        
        [Parameter()]
        [switch]$ResetSecretStore,
        
        [Parameter()]
        [switch]$GlobalVault
    )

    begin {
        $correlationId = [System.Guid]::NewGuid()
        Write-Verbose "Starting Remove-ADEnvironment - CorrelationId: $correlationId"
        
        # Get domain information
        $domain = Get-ADTestDomain
        
        # Safety check - confirm this is a test environment.
        #
        # Skipped under -WhatIf: a preview changes nothing, so demanding that someone type
        # CONFIRM before being shown what would happen is backwards, and it made -WhatIf
        # unusable from anything non-interactive.
        if (-not $Force -and -not $WhatIfPreference) {
            Write-Warning "This will permanently delete all test data from Active Directory."
            Write-Warning "Domain: $($domain.DNSName)"
            if ($RemoveOUs) {
                Write-Warning "OU REMOVAL ENABLED: This will also delete the entire test OU structure!"
            }

            $confirmation = Read-Host "Type 'CONFIRM' to proceed with deletion"
            if ($confirmation -ne 'CONFIRM') {
                Write-Host "Operation cancelled by user." -ForegroundColor Yellow
                return @{ Cancelled = $true }
            }
        }

        # One confirmation covers the whole run.
        #
        # This is what the guards below used to express as "$Force -or $manuallyConfirmed -or
        # ShouldProcess(...)", and that short-circuit was a serious bug: with -Force the first
        # operand was true, ShouldProcess was never consulted, and -WhatIf was ignored -
        # "Remove-ADEnvironment -RemoveOUs -Force -WhatIf" really deleted the directory.
        #
        # Suppressing ConfirmPreference instead keeps the intent (ask once, not once per
        # object) while leaving ShouldProcess as the single gate, so -WhatIf always wins and
        # emits the standard "What if:" line for every object.
        $ConfirmPreference = 'None'

        # Counters
        $script:UsersRemoved = 0
        $script:DevicesRemoved = 0
        $script:GroupsRemoved = 0
        $script:OUsRemoved = 0
        $script:PoliciesRemoved = 0
        $script:GroupPoliciesRemoved = 0
        $script:VaultsRemoved = 0
        $script:SecretsRemoved = 0
        $script:Errors = @()
    }

    process {
        try {
            Write-TestMessage -Message "Removing Active Directory Test Environment" -Type Header

            # Anything of ours that is no longer where we put it. The sweeps below all search the
            # seeded container, so an object moved out of it becomes unmanaged - created by this
            # module, invisible to its teardown, and left behind for somebody to find by hand.
            # That is not hypothetical: a faulty run once left 296 seeded accounts in the default
            # Users container, and clearing them took a query written by hand.
            #
            # The tag makes this safe to do domain-wide. Only objects carrying it are named, and
            # they are only ever reported here - removal still happens in the typed sweeps below,
            # which know how to deal with each class.
            $strayFilter = "(adminDescription=$((Get-ADTestSeedMarker).Tag))"
            $strays = @(
                Get-ADObject -LDAPFilter $strayFilter -SearchBase $domain.DomainDN -ErrorAction SilentlyContinue |
                    Where-Object { $_.DistinguishedName -notlike "*OU=$($script:ADTestRootName),$($domain.DomainDN)" }
            )

            if ($strays.Count -gt 0) {
                Write-Warning ("$($strays.Count) seeded object(s) carry the seed tag but sit outside " +
                    "OU=$($script:ADTestRootName). They were created by this module and moved since. " +
                    'Listed below; remove them by hand or move them back and re-run.')
                foreach ($stray in $strays) {
                    Write-Warning " $($stray.ObjectClass): $($stray.DistinguishedName)"
                }
            }

            # Step 1: Remove Test Users
            Write-TestMessage -Message "Removing test users..." -Type Info
            try {
                # Remove all users from TestData OU structure (excluding built-in accounts)
                $searchBase = "OU=$($script:ADTestRootName),$($domain.DomainDN)"
                $testUsers = @(Get-ADUser -Filter '*' -SearchBase $searchBase -Properties adminDescription -ErrorAction SilentlyContinue |
                    Select-ADTestOwnedObject -Kind user)
                
                foreach ($user in $testUsers) {
                    if ($PSCmdlet.ShouldProcess($user.Name, "Remove AD User")) {
                        try {
                            Remove-ADUser -Identity $user.DistinguishedName -Confirm:$false
                            Write-Verbose "Removed user: $($user.Name)"
                            $script:UsersRemoved++
                        }
                        catch {
                            Write-Warning "Failed to remove user $($user.Name): $($_.Exception.Message)"
                            $script:Errors += "User removal error: $($user.Name)"
                        }
                    }
                }
            }
            catch {
                if ($_.Exception.Message -like "*Directory object not found*") {
                    Write-Verbose "No test users found (TestData OU may not exist)"
                } else {
                    Write-Warning "Error searching for test users: $($_.Exception.Message)"
                }
                $script:Errors += "User search error: $($_.Exception.Message)"
            }
            
            # Step 2: Remove Test Devices
            Write-TestMessage -Message "Removing test devices..." -Type Info
            try {
                # Remove all devices from TestData OU structure
                $searchBase = "OU=$($script:ADTestRootName),$($domain.DomainDN)"
                $testDevices = @(Get-ADComputer -Filter '*' -SearchBase $searchBase -Properties adminDescription -ErrorAction SilentlyContinue |
                    Select-ADTestOwnedObject -Kind computer)
                
                foreach ($device in $testDevices) {
                    if ($PSCmdlet.ShouldProcess($device.Name, "Remove AD Computer")) {
                        try {
                            Remove-ADComputer -Identity $device.DistinguishedName -Confirm:$false
                            Write-Verbose "Removed device: $($device.Name)"
                            $script:DevicesRemoved++
                        }
                        catch {
                            Write-Warning "Failed to remove device $($device.Name): $($_.Exception.Message)"
                            $script:Errors += "Device removal error: $($device.Name)"
                        }
                    }
                }
            }
            catch {
                if ($_.Exception.Message -like "*Directory object not found*") {
                    Write-Verbose "No test devices found (TestData OU may not exist)"
                } else {
                    Write-Warning "Error searching for test devices: $($_.Exception.Message)"
                }
                $script:Errors += "Device search error: $($_.Exception.Message)"
            }
            
            # Step 3: Remove Test Service Accounts
            Write-TestMessage -Message "Removing test service accounts..." -Type Info
            try {
                # Remove all service accounts from ServiceAccounts OU
                $svcQuery = @{
                    Filter      = '*'
                    SearchBase  = "OU=ServiceAccounts,OU=$($script:ADTestRootName),$($domain.DomainDN)"
                    ErrorAction = 'SilentlyContinue'
                    Properties  = 'adminDescription'
                }
                $testServiceAccounts = @(Get-ADUser @svcQuery | Select-ADTestOwnedObject -Kind 'service account')
                
                foreach ($serviceAccount in $testServiceAccounts) {
                    if ($PSCmdlet.ShouldProcess($serviceAccount.Name, "Remove AD Service Account")) {
                        try {
                            Remove-ADUser -Identity $serviceAccount.DistinguishedName -Confirm:$false
                            Write-Verbose "Removed service account: $($serviceAccount.Name)"
                            $script:UsersRemoved++
                        }
                        catch {
                            Write-Warning ("Failed to remove service account $($serviceAccount.Name): " +
                                "$($_.Exception.Message)")
                            $script:Errors += "Service account removal error: $($serviceAccount.Name)"
                        }
                    }
                }
            }
            catch {
                if ($_.Exception.Message -like "*Directory object not found*") {
                    Write-Verbose "No test service accounts found (ServiceAccounts OU may not exist)"
                } else {
                    Write-Warning "Error searching for test service accounts: $($_.Exception.Message)"
                }
                $script:Errors += "Service account search error: $($_.Exception.Message)"
            }
            
            # Step 4: Remove Test Security Groups
            Write-TestMessage -Message "Removing test security groups..." -Type Info
            try {
                # Searched across the whole of OU=TestData, not just OU=Groups.
                #
                # The users sweep above already works this way; groups did not, so any group
                # living outside OU=Groups survived a teardown that was not given -RemoveOUs.
                # The edge case groups are the concrete case - they sit under OU=EdgeCases -
                # but it applies to anything placed elsewhere in the tree later.
                $searchBase = "OU=$($script:ADTestRootName),$($domain.DomainDN)"
                $testGroups = @(Get-ADGroup -Filter '*' -SearchBase $searchBase -Properties adminDescription -ErrorAction SilentlyContinue |
                    Select-ADTestOwnedObject -Kind group)
                
                foreach ($group in $testGroups) {
                    if ($PSCmdlet.ShouldProcess($group.Name, "Remove AD Group")) {
                        try {
                            Remove-ADGroup -Identity $group.DistinguishedName -Confirm:$false
                            Write-Verbose "Removed group: $($group.Name)"
                            $script:GroupsRemoved++
                        }
                        catch {
                            Write-Warning "Failed to remove group $($group.Name): $($_.Exception.Message)"
                            $script:Errors += "Group removal error: $($group.Name)"
                        }
                    }
                }
            }
            catch {
                if ($_.Exception.Message -like "*Directory object not found*") {
                    Write-Verbose "No test groups found (Groups OU may not exist)"
                } else {
                    Write-Warning "Error searching for test groups: $($_.Exception.Message)"
                }
                $script:Errors += "Group search error: $($_.Exception.Message)"
            }
            
            # Step 4b: Remove edge case password settings objects
            #
            # A password settings object lives in CN=Password Settings Container,CN=System,
            # not under OU=TestData, so it is the one thing New-ADTestEdgeCase creates that
            # the recursive OU delete cannot reach. Removed by name prefix rather than by
            # location, and only ones this module creates.
            Write-TestMessage -Message "Removing edge case password settings objects..." -Type Info
            try {
                $testPolicies = @(Get-ADFineGrainedPasswordPolicy -Filter ("Name -like " +
                    "'EdgeCase*'") -ErrorAction SilentlyContinue)

                foreach ($policy in $testPolicies) {
                    if ($PSCmdlet.ShouldProcess($policy.Name, "Remove AD Fine-Grained Password Policy")) {
                        try {
                            Remove-ADFineGrainedPasswordPolicy -Identity $policy.DistinguishedName -Confirm:$false
                            Write-Verbose "Removed password settings object: $($policy.Name)"
                            $script:PoliciesRemoved++
                        }
                        catch {
                            Write-Warning ("Failed to remove password policy $($policy.Name): " +
                                "$($_.Exception.Message)")
                            $script:Errors += "Password policy removal error: $($policy.Name)"
                        }
                    }
                }
            }
            catch {
                Write-Verbose "No edge case password settings objects found: $($_.Exception.Message)"
            }

            # The companion deny-logon policy lives in CN=Policies,CN=System, so like the
            # password settings objects above it is outside OU=TestData and a recursive OU
            # delete cannot reach it. Removed before the OUs so the link is unwound while
            # its target still exists, rather than leaving an orphaned policy behind.
            #
            # Matched on the marker in the comment as well as the name: deleting a GPO is
            # not recoverable, and a real policy that happened to share the name must not
            # be removed by a test teardown.
            Write-TestMessage -Message "Removing test Group Policy objects..." -Type Info
            try {
                Import-Module GroupPolicy -ErrorAction Stop -Verbose:$false

                $policy = Get-ADTestPolicySetting -DomainDN $domain.DomainDN
                $testGpo = Get-GPO -Name $policy.Name -ErrorAction SilentlyContinue

                if (-not $testGpo) {
                    Write-Verbose "No test Group Policy found (already clean)"
                }
                elseif ([string]$testGpo.Description -notlike "*$($policy.Marker)*") {
                    $skipMsg = "A GPO named '$($policy.Name)' exists but does not carry " +
                               'this module marker, so it was left alone'
                    Write-Warning $skipMsg
                    $script:Errors += $skipMsg
                }
                elseif ($PSCmdlet.ShouldProcess($policy.Name, 'Remove Group Policy object')) {
                    # Unlink first where the target still exists. Remove-GPO drops the links
                    # too, but doing it explicitly keeps the intent readable and survives a
                    # partially removed OU tree.
                    $inheritance = Get-GPInheritance -Target $policy.LinkTarget -ErrorAction SilentlyContinue
                    if ($inheritance -and @($inheritance.GpoLinks |
                            Where-Object { $_.DisplayName -eq $policy.Name }).Count -gt 0) {
                        Remove-GPLink -Guid $testGpo.Id -Target $policy.LinkTarget -ErrorAction SilentlyContinue |
                            Out-Null
                        Write-Verbose "Unlinked from $($policy.LinkTarget)"
                    }

                    Remove-GPO -Guid $testGpo.Id -ErrorAction Stop
                    $script:GroupPoliciesRemoved++
                    Write-Verbose "Removed Group Policy: $($policy.Name)"
                }
            }
            catch [System.IO.FileNotFoundException] {
                Write-Verbose "GroupPolicy module not available - skipping policy removal"
            }
            catch {
                Write-Warning "Failed to remove test Group Policy: $($_.Exception.Message)"
                $script:Errors += "Group Policy removal error: $($_.Exception.Message)"
            }

            # Step 5: Remove OUs (if requested)
            if ($RemoveOUs) {
                Write-TestMessage -Message "Removing test OU structure..." -Type Info
                
                if ($RemoveOUs) {
                    try {
                        # Remove OUs in reverse hierarchical order
                        $ouRemovalOrder = @(
                            "OU=Administrative,OU=Groups,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Resource,OU=Groups,OU=$($script:ADTestRootName),$($domain.DomainDN)", 
                            "OU=Device,OU=Groups,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Location,OU=Groups,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Role,OU=Groups,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Department,OU=Groups,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Groups,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Workstations,OU=Devices,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Servers,OU=Devices,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Printers,OU=Devices,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Mobile,OU=Devices,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=Devices,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=ServiceAccounts,OU=$($script:ADTestRootName),$($domain.DomainDN)"
                        )
                        
                        # Add edge case OUs (dynamically discovered, deepest first).
                        #
                        # These hold the states New-ADTestEdgeCase creates. Two of them - a
                        # delegated access control entry and a planted orphaned SID - live in
                        # security descriptors rather than being objects, so they are only
                        # really gone once the OUs holding them are gone. Discovered rather
                        # than hardcoded so an edge case added later is still cleaned up, and
                        # sorted by depth so children are removed before their parents.
                        try {
                            $edgeCaseRoot = "OU=EdgeCases,OU=$($script:ADTestRootName),$($domain.DomainDN)"

                            # -SearchBase includes the base itself, so this returns the
                            # EdgeCases root along with its children and no separate entry
                            # for it is needed.
                            $edgeQuery = @{
                                Filter      = '*'
                                SearchBase  = $edgeCaseRoot
                                ErrorAction = 'SilentlyContinue'
                            }
                            $edgeOUs = @(Get-ADOrganizationalUnit @edgeQuery |
                                Sort-Object { ($_.DistinguishedName -split ',').Count } -Descending)

                            # Prepended in one go. Prepending inside a foreach reverses the
                            # sort - each item lands in front of the previous one - which put
                            # the root first and left every child entry to be logged as
                            # "already removed" after the recursive delete had taken it.
                            if ($edgeOUs.Count -gt 0) {
                                $ouRemovalOrder = @($edgeOUs.DistinguishedName) + $ouRemovalOrder
                            }
                        }
                        catch {
                            Write-Verbose "No edge case OUs found or error accessing them"
                        }

                        # Add department/user OUs (dynamically discovered)
                        try {
                            $userOUQuery = @{
                                Filter      = '*'
                                SearchBase  = "OU=Users,OU=$($script:ADTestRootName),$($domain.DomainDN)"
                                ErrorAction = 'SilentlyContinue'
                            }
                            $userOUs = Get-ADOrganizationalUnit @userOUQuery
                            foreach ($userOU in $userOUs) {
                                if ($userOU.DistinguishedName -ne "OU=Users,OU=$($script:ADTestRootName),$($domain.DomainDN)") {
                                    $ouRemovalOrder = @($userOU.DistinguishedName) + $ouRemovalOrder
                                }
                            }
                        }
                        catch {
                            Write-Verbose "No user department OUs found or error accessing them"
                        }
                        
                        # Add the main structure OUs
                        $ouRemovalOrder += @(
                            "OU=Users,OU=$($script:ADTestRootName),$($domain.DomainDN)",
                            "OU=$($script:ADTestRootName),$($domain.DomainDN)"
                        )
                        
                        foreach ($ouPath in $ouRemovalOrder) {
                            if ($PSCmdlet.ShouldProcess($ouPath, "Remove AD Organizational Unit")) {
                                try {
                                    # Check if OU exists before trying to remove
                                    $ou = Get-ADOrganizationalUnit -Identity $ouPath -ErrorAction SilentlyContinue

                                    # -Recursive deletes whatever is still inside, which would
                                    # undo the ownership check the sweeps above just applied: a
                                    # group this module refused to delete would die anyway when
                                    # its container went. Verified against a live domain, where
                                    # exactly that happened - teardown warned that it was
                                    # sparing an object and then removed it seconds later.
                                    #
                                    # So an organisational unit still holding anything untagged
                                    # is left standing, along with its contents. That makes
                                    # -RemoveOUs best-effort rather than absolute, which is the
                                    # correct trade: an incomplete teardown is recoverable and a
                                    # deleted production group is not.
                                    $foreign = @()
                                    if ($ou) {
                                        $foreign = @(
                                            Get-ADObject -Filter * -SearchBase $ouPath -Properties adminDescription -ErrorAction SilentlyContinue |
                                                Where-Object {
                                                    $_.DistinguishedName -ne $ouPath -and
                                                    $_.adminDescription -ne (Get-ADTestSeedMarker).Tag
                                                }
                                        )
                                    }

                                    if ($foreign.Count -gt 0) {
                                        Write-Warning ("Not removing $ouPath : it still holds $($foreign.Count) object(s) " +
                                            'this module cannot prove it created. Remove them or move them out, then re-run.')
                                        foreach ($item in $foreign) {
                                            Write-Warning " $($item.ObjectClass): $($item.DistinguishedName)"
                                        }
                                        continue
                                    }

                                    if ($ou) {
                                        # Enable deletion by removing protection. -WhatIf:$false
                                        # is NOT used here on purpose: this whole branch is
                                        # already behind ShouldProcess, so under -WhatIf it
                                        # never runs and the protection flag is left alone.
                                        $setADOrganizationalUnitArgs1 = @{
                                            Identity                        = $ouPath
                                            ProtectedFromAccidentalDeletion = $false
                                            ErrorAction                     = 'SilentlyContinue'
                                        }
                                        Set-ADOrganizationalUnit @setADOrganizationalUnitArgs1
                                        Remove-ADOrganizationalUnit -Identity $ouPath -Recursive -Confirm:$false
                                        Write-Verbose "Removed OU: $ouPath"
                                        $script:OUsRemoved++
                                    }
                                }
                                catch {
                                    # Only warn for actual failures, not missing OUs
                                    if ($_.Exception.Message -notmatch "Directory object not found") {
                                        Write-Warning "Failed to remove OU $ouPath : $($_.Exception.Message)"
                                        $script:Errors += "OU removal error: $ouPath"
                                    }
                                    else {
                                        Write-Verbose "OU not found (already removed): $ouPath"
                                    }
                                }
                            }
                        }
                    }
                    catch {
                        # Only warn for actual failures, not missing search bases
                        if ($_.Exception.Message -notmatch "Directory object not found") {
                            Write-Warning "Error during OU removal: $($_.Exception.Message)"
                            $script:Errors += "OU removal process error: $($_.Exception.Message)"
                        }
                        else {
                            Write-Verbose "OU structure not found (already clean): $($_.Exception.Message)"
                        }
                    }
                }
            }
            
            # Remove SecretStore vault if it exists
            try {
                Write-TestMessage -Message "Checking for SecretStore vault removal..." -Type Info
                
                # Same fix as the AD guards above. This one mattered doubly: with -Force the
                # short-circuit meant Remove-ADTestSecretVault was invoked for real under
                # -WhatIf, destroying stored secrets during what was meant to be a preview.
                if ($PSCmdlet.ShouldProcess("SecretStore Vault: $VaultName", "Remove Secret Vault")) {
                    $removeADTestSecretVaultArgs2 = @{
                        VaultName        = $VaultName
                        Force            = $Force
                        ResetSecretStore = $ResetSecretStore
                        GlobalVault      = $GlobalVault
                    }
                    $vaultResult = Remove-ADTestSecretVault @removeADTestSecretVaultArgs2
                    
                    if ($vaultResult.VaultRemoved) {
                        $script:VaultsRemoved++
                        $script:SecretsRemoved += $vaultResult.SecretsRemoved
                        Write-Host " Removed SecretStore vault: $VaultName" -ForegroundColor Green
                        Write-Host " Removed $($vaultResult.SecretsRemoved) stored secrets" -ForegroundColor Green
                        
                        if ($vaultResult.SecretStoreReset) {
                            Write-Host " Reset SecretStore configuration to defaults" -ForegroundColor Green
                        }
                    }
                    elseif ($vaultResult.VaultExists -eq $false) {
                        Write-Host " SecretStore vault '$VaultName' was not found" -ForegroundColor Yellow
                    }
                    
                    if ($vaultResult.Errors.Count -gt 0) {
                        $vaultResult.Errors | ForEach-Object { 
                            Write-Warning "Vault removal error: $_"
                            $script:Errors += "Vault removal: $_"
                        }
                    }
                }
            }
            catch {
                Write-Warning "Error during vault removal: $($_.Exception.Message)"
                $script:Errors += "Vault removal process error: $($_.Exception.Message)"
            }
            
            # Create summary
            $results = @{
                CorrelationId = $correlationId
                UsersRemoved = $script:UsersRemoved
                DevicesRemoved = $script:DevicesRemoved
                GroupsRemoved = $script:GroupsRemoved
                OUsRemoved = $script:OUsRemoved
                PoliciesRemoved = $script:PoliciesRemoved
                GroupPoliciesRemoved = $script:GroupPoliciesRemoved
                VaultsRemoved = $script:VaultsRemoved
                SecretsRemoved = $script:SecretsRemoved
                OUsRequested = $RemoveOUs
                Errors = $script:Errors
                TotalRemoved = $script:UsersRemoved + $script:DevicesRemoved +
                               $script:GroupsRemoved + $script:OUsRemoved +
                               $script:VaultsRemoved
            }
            
            # Display summary
            Write-TestMessage -Message "Test Environment Removal Summary" -Type Success
            Write-Host " Users Removed: $($results.UsersRemoved)" -ForegroundColor Green
            Write-Host " Devices Removed: $($results.DevicesRemoved)" -ForegroundColor Green
            Write-Host " Groups Removed: $($results.GroupsRemoved)" -ForegroundColor Green
            if ($RemoveOUs) {
                Write-Host " OUs Removed: $($results.OUsRemoved)" -ForegroundColor Green
            }
            if ($results.PoliciesRemoved -gt 0) {
                Write-Host (" Password Settings Objects Removed: " +
                    "$($results.PoliciesRemoved)") -ForegroundColor Green
            }
            if ($results.GroupPoliciesRemoved -gt 0) {
                Write-Host (" Group Policy Objects Removed: " +
                    "$($results.GroupPoliciesRemoved)") -ForegroundColor Green
            }
            if ($results.VaultsRemoved -gt 0) {
                Write-Host " SecretStore Vaults Removed: $($results.VaultsRemoved)" -ForegroundColor Green
            }
            if ($results.SecretsRemoved -gt 0) {
                Write-Host " Secrets Removed: $($results.SecretsRemoved)" -ForegroundColor Green
            }
            Write-Host " Total Objects Removed: $($results.TotalRemoved)" -ForegroundColor Cyan
            
            if ($results.Errors.Count -gt 0) {
                Write-Host " Errors: $($results.Errors.Count)" -ForegroundColor Red
                $results.Errors | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
            }
            else {
                Write-Host " No errors encountered!" -ForegroundColor Green
            }
            
            # Return detailed results for programmatic access only
            if ($PassThru) {
                return [PSCustomObject]$results
            }
            # Store results in verbose output for troubleshooting
            Write-Verbose "Detailed results: $($results | ConvertTo-Json -Depth 3)"
            
        } catch {
            Write-Error "Failed to remove test environment: $($_.Exception.Message)" -ErrorAction Stop
        }
    }

    end {
        Write-Verbose "Completed Remove-ADEnvironment - CorrelationId: $correlationId"
    }
}