modules/Devolutions.CIEM.PSU/Tests/Unit/InvokeCIEMJobWithProgress.Tests.ps1

BeforeAll {
    Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue
    Import-Module (Join-Path $PSScriptRoot '..' '..' '..' '..' 'Devolutions.CIEM.psd1')
    New-CIEMDatabase -Path "$TestDrive/ciem.db"
    InModuleScope Devolutions.CIEM { $script:DatabasePath = "$TestDrive/ciem.db" }
    Mock -ModuleName Devolutions.CIEM Write-CIEMLog {}

    # Mock PSU UI cmdlets (not available outside PSU)
    Mock -ModuleName Devolutions.CIEM Set-UDElement {}
    Mock -ModuleName Devolutions.CIEM New-UDCard { 'card' }
    Mock -ModuleName Devolutions.CIEM New-UDProgress { 'progress' }
    Mock -ModuleName Devolutions.CIEM New-UDStack { 'stack' }
    Mock -ModuleName Devolutions.CIEM New-UDTypography { 'typo' }
}

Describe 'Invoke-CIEMJobWithProgress' {

    Context 'Command structure' {

        It 'Is available as a public command' {
            Get-Command Invoke-CIEMJobWithProgress -Module Devolutions.CIEM -ErrorAction Stop | Should -Not -BeNullOrEmpty
        }

        It 'Has mandatory -ScriptName parameter' {
            $param = (Get-Command Invoke-CIEMJobWithProgress).Parameters['ScriptName']
            $param | Should -Not -BeNullOrEmpty
            $mandatory = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory }
            $mandatory | Should -Not -BeNullOrEmpty
        }

        It 'Has mandatory -ProgressElementId parameter' {
            $param = (Get-Command Invoke-CIEMJobWithProgress).Parameters['ProgressElementId']
            $param | Should -Not -BeNullOrEmpty
            $mandatory = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory }
            $mandatory | Should -Not -BeNullOrEmpty
        }

        It 'Has optional -PollIntervalSeconds parameter defaulting to 3' {
            $param = (Get-Command Invoke-CIEMJobWithProgress).Parameters['PollIntervalSeconds']
            $param | Should -Not -BeNullOrEmpty
        }

        It 'Has optional -MaxPollSeconds parameter defaulting to 1800' {
            $param = (Get-Command Invoke-CIEMJobWithProgress).Parameters['MaxPollSeconds']
            $param | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Completed job returns pipeline output' {

        It 'Returns output from a successfully completed job' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 1001 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 1001; Status = 'Completed'; Activity = 'Done'; PercentComplete = 100; StatusDescription = ''; CurrentOperation = '' }
            }
            Mock -ModuleName Devolutions.CIEM Get-PSUJobPipelineOutput {
                [PSCustomObject]@{ Result = 'success' }
            }

            $result = Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'prog1' -PollIntervalSeconds 0 -MaxPollSeconds 10
            $result.Result | Should -Be 'success'
        }
    }

    Context 'Warning status treated as success' {

        It 'Returns output when job status is Warning' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 1002 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 1002; Status = 'Warning'; Activity = ''; PercentComplete = 100; StatusDescription = ''; CurrentOperation = '' }
            }
            Mock -ModuleName Devolutions.CIEM Get-PSUJobPipelineOutput {
                [PSCustomObject]@{ Result = 'with-warnings' }
            }

            $result = Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'prog2' -PollIntervalSeconds 0 -MaxPollSeconds 10
            $result.Result | Should -Be 'with-warnings'
        }
    }

    Context 'Failed job throws' {

        It 'Throws when job status is Failed' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 1003 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 1003; Status = 'Failed'; Activity = ''; PercentComplete = 0; StatusDescription = ''; CurrentOperation = '' }
            }
            Mock -ModuleName Devolutions.CIEM Get-PSUJobOutput { 'Something broke' }

            { Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'prog3' -PollIntervalSeconds 0 -MaxPollSeconds 10 } | Should -Throw '*Something broke*'
        }
    }

    Context 'Canceled job throws' {

        It 'Throws when job status is Canceled' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 1004 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 1004; Status = 'Canceled'; Activity = ''; PercentComplete = 0; StatusDescription = ''; CurrentOperation = '' }
            }

            { Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'prog4' -PollIntervalSeconds 0 -MaxPollSeconds 10 } | Should -Throw '*cancelled*'
        }
    }

    Context 'Button disable/enable' {

        It 'Disables and re-enables buttons during execution' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 1005 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 1005; Status = 'Completed'; Activity = ''; PercentComplete = 100; StatusDescription = ''; CurrentOperation = '' }
            }
            Mock -ModuleName Devolutions.CIEM Get-PSUJobPipelineOutput { 'ok' }

            Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'prog5' -DisableElementIds @('btn1', 'btn2') -PollIntervalSeconds 0 -MaxPollSeconds 10

            # Verify Set-UDElement was called to disable (at start) and enable (at end) each button
            Should -Invoke -CommandName Set-UDElement -ModuleName Devolutions.CIEM -ParameterFilter { $Id -eq 'btn1' -and $Properties.disabled -eq $true } -Times 1
            Should -Invoke -CommandName Set-UDElement -ModuleName Devolutions.CIEM -ParameterFilter { $Id -eq 'btn1' -and $Properties.disabled -eq $false } -Times 1
            Should -Invoke -CommandName Set-UDElement -ModuleName Devolutions.CIEM -ParameterFilter { $Id -eq 'btn2' -and $Properties.disabled -eq $true } -Times 1
            Should -Invoke -CommandName Set-UDElement -ModuleName Devolutions.CIEM -ParameterFilter { $Id -eq 'btn2' -and $Properties.disabled -eq $false } -Times 1
        }
    }

    Context 'Error job throws' {

        It 'Throws when job status is Error' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 2001 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 2001; Status = 'Error'; Activity = ''; PercentComplete = 0; StatusDescription = ''; CurrentOperation = '' }
            }
            Mock -ModuleName Devolutions.CIEM Get-PSUJobOutput { 'Error occurred' }

            { Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'progErr' -PollIntervalSeconds 0 -MaxPollSeconds 10 } | Should -Throw '*Error occurred*'
        }
    }

    Context 'TimedOut job throws' {

        It 'Throws when job status is TimedOut' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 2002 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 2002; Status = 'TimedOut'; Activity = ''; PercentComplete = 0; StatusDescription = ''; CurrentOperation = '' }
            }

            { Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'progTO' -PollIntervalSeconds 0 -MaxPollSeconds 10 } | Should -Throw '*timed out*'
        }
    }

    Context 'Unexpected status throws' {

        It 'Throws with status name for unexpected terminal status' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 2003 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 2003; Status = 'SomethingUnexpected'; Activity = ''; PercentComplete = 0; StatusDescription = ''; CurrentOperation = '' }
            }

            { Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'progUX' -PollIntervalSeconds 0 -MaxPollSeconds 10 } | Should -Throw '*unexpected status*SomethingUnexpected*'
        }
    }

    Context 'Button re-enable on failure' {

        It 'Re-enables buttons even when job fails' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 2004 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 2004; Status = 'Failed'; Activity = ''; PercentComplete = 0; StatusDescription = ''; CurrentOperation = '' }
            }
            Mock -ModuleName Devolutions.CIEM Get-PSUJobOutput { 'Crashed' }

            try {
                Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'progFail' -DisableElementIds @('btn-fail') -PollIntervalSeconds 0 -MaxPollSeconds 10
            } catch {}

            # Finally block should have re-enabled the button even after failure
            Should -Invoke -CommandName Set-UDElement -ModuleName Devolutions.CIEM -ParameterFilter { $Id -eq 'btn-fail' -and $Properties.disabled -eq $false } -Times 1
        }
    }

    Context 'StatusDescription in progress text' {

        It 'Logs StatusDescription when present in job progress' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 2005 }
            }
            $script:callCount = 0
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                $script:callCount++
                if ($script:callCount -ge 2) {
                    [PSCustomObject]@{ Id = 2005; Status = 'Completed'; Activity = 'Done'; PercentComplete = 100; StatusDescription = ''; CurrentOperation = '' }
                } else {
                    [PSCustomObject]@{ Id = 2005; Status = 'Running'; Activity = 'Collecting'; PercentComplete = 50; StatusDescription = 'Collecting Entra data'; CurrentOperation = 'Users' }
                }
            }
            Mock -ModuleName Devolutions.CIEM Get-PSUJobPipelineOutput { 'ok' }

            Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'progSD' -PollIntervalSeconds 0 -MaxPollSeconds 10

            # Verify Write-CIEMLog was called with a message containing the StatusDescription
            Should -Invoke -CommandName Write-CIEMLog -ModuleName Devolutions.CIEM -ParameterFilter { $Message -like '*statusDesc=Collecting Entra data*' } -Times 1
        }
    }

    Context 'Poll timeout' {

        It 'Throws when MaxPollSeconds is exceeded' {
            Mock -ModuleName Devolutions.CIEM Invoke-PSUScript {
                [PSCustomObject]@{ Id = 1006 }
            }
            Mock -ModuleName Devolutions.CIEM Start-Sleep {}
            Mock -ModuleName Devolutions.CIEM Get-PSUJob {
                [PSCustomObject]@{ Id = 1006; Status = 'Running'; Activity = 'Still going'; PercentComplete = 50; StatusDescription = ''; CurrentOperation = '' }
            }

            # MaxPollSeconds = 1, PollIntervalSeconds = 1 → after 1 poll, elapsed >= Max
            { Invoke-CIEMJobWithProgress -ScriptName 'Test\Script' -ProgressElementId 'prog6' -PollIntervalSeconds 1 -MaxPollSeconds 1 } | Should -Throw '*timed out*'
        }
    }
}