Public/Test-EnvkInstallation.ps1


#Requires -Version 5.1
<#
.SYNOPSIS
    Checks installation health and reports results as a structured summary.
 
.DESCRIPTION
    Test-EnvkInstallation verifies four areas of installation health:
 
    1. Module loading: PowerShell version (5.1 minimum, 7+ for parallel features)
       and UIAutomationClient assembly availability.
    2. Config validity: Config file found and parseable via Get-EnvkConfig;
       all app paths resolvable via Resolve-EnvkExecutablePath.
    3. Task Scheduler configuration: Scheduled task exists whose action references
       Envoke.vbs; ScheduledTasks module available.
    4. Write permissions: Log directory writable; registry path accessible.
 
    Each check produces a PSCustomObject with Name, Status (Pass/Fail/Warn), and
    Message properties. After all checks run, a formatted summary table is written
    to the console via Write-EnvkLog.
 
    The function is report-only -- it does not modify system state or auto-fix
    any issues. Failed and Warn results include actionable messages telling the
    user what command to run.
 
.PARAMETER ConfigPath
    Optional path to a config.json file. When omitted, Get-EnvkConfig uses its
    default three-location search order. Passed directly to Get-EnvkConfig.
 
.OUTPUTS
    [PSCustomObject[]]
    Array of check result objects. Each object has:
      Name [string] -- Check area label
      Status [string] -- 'Pass' | 'Fail' | 'Warn'
      Message [string] -- Human-readable detail; actionable for non-Pass results
 
.EXAMPLE
    # Run installation checks with default config search
    $results = Test-EnvkInstallation
 
.EXAMPLE
    # Run checks against an explicit config path
    $results = Test-EnvkInstallation -ConfigPath 'C:\repos\Envoke\config.json'
 
.EXAMPLE
    # Filter for only non-passing checks
    Test-EnvkInstallation | Where-Object { $_.Status -ne 'Pass' }
 
.NOTES
    Author: Aaron AlAnsari
    Created: 2026-02-26
#>


# PSUseOutputTypeCorrectly: unary comma forces Object[] wrapper to prevent single-element
# unboxing in the pipeline. Element type is always PSCustomObject.
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
param ()

function Test-EnvkInstallation {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter()]
        [string]$ConfigPath
    )

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Private helper: adds a single check result to the list.
    function Add-Check {
        param (
            [string]$Name,
            [string]$Status,
            [string]$Message
        )
        $results.Add([PSCustomObject]@{
            Name    = $Name
            Status  = $Status
            Message = $Message
        })
    }

    # -------------------------------------------------------------------------
    # Area 1: Module loading
    # -------------------------------------------------------------------------

    # -- PS version check
    $psMajor = Get-PSMajorVersion
    if ($psMajor -lt 5) {
        Add-Check -Name 'PS version' -Status 'Fail' `
            -Message "PowerShell $psMajor is below minimum required 5.1."
    }
    elseif ($psMajor -ge 7) {
        Add-Check -Name 'PS version' -Status 'Pass' `
            -Message "PowerShell $psMajor -- parallel launching available."
    }
    else {
        Add-Check -Name 'PS version' -Status 'Warn' `
            -Message "PowerShell 5.1 detected -- parallel launching unavailable. Install PowerShell 7+ for parallel mode."
    }

    # -- UIAutomationClient assembly check
    try {
        Invoke-LoadUIAutomationAssembly
        Add-Check -Name 'UIAutomation assembly' -Status 'Pass' `
            -Message 'UIAutomationClient assembly loaded.'
    }
    catch {
        Add-Check -Name 'UIAutomation assembly' -Status 'Warn' `
            -Message 'UIAutomationClient assembly not available -- window maximization will use WinAPI fallback only.'
    }

    # -------------------------------------------------------------------------
    # Area 2: Config validity
    # -------------------------------------------------------------------------

    $loadedConfig = $null

    # -- Config file found and parseable
    $configLoadParams = @{}
    if (-not [string]::IsNullOrEmpty($ConfigPath)) {
        $configLoadParams['ConfigPath'] = $ConfigPath
    }

    try {
        $loadedConfig = Get-EnvkConfig @configLoadParams
        Add-Check -Name 'Config file' -Status 'Pass' `
            -Message "Configuration loaded successfully."
    }
    catch {
        Add-Check -Name 'Config file' -Status 'Fail' `
            -Message $_.Exception.Message
    }

    # -- App paths resolvable (only if config loaded successfully)
    if ($null -ne $loadedConfig) {
        $appCount   = 0
        $failedApps = [System.Collections.Generic.List[PSCustomObject]]::new()

        $appProps = $loadedConfig.apps.PSObject.Properties
        foreach ($prop in $appProps) {
            $appCount++
            $appEntry = $prop.Value
            $resolved = Resolve-EnvkExecutablePath -Path $appEntry.path
            if ($null -eq $resolved -or $resolved.Valid -eq $false) {
                $reason = if ($null -eq $resolved) { 'Could not resolve path' } else { $resolved.Reason }
                $failedApps.Add([PSCustomObject]@{
                    Name   = $prop.Name
                    Path   = $appEntry.path
                    Reason = $reason
                })
            }
        }

        if ($failedApps.Count -gt 0) {
            $details = $failedApps | ForEach-Object { "'$($_.Name)': $($_.Reason)" }
            $msg = "App path(s) not found: $($details -join '; '). Verify paths in your config."
            Add-Check -Name 'App paths' -Status 'Warn' `
                -Message $msg
        }
        else {
            Add-Check -Name 'App paths' -Status 'Pass' `
                -Message "All $appCount app path(s) verified."
        }
    }
    else {
        Add-Check -Name 'App paths' -Status 'Warn' `
            -Message 'App path check skipped -- config file could not be loaded.'
    }

    # -------------------------------------------------------------------------
    # Area 3: Task Scheduler configuration
    # -------------------------------------------------------------------------

    $taskFound = $null
    try {
        $allTasks = Get-ScheduledTask -ErrorAction Stop
        $taskFound = $allTasks | Where-Object {
            $_.Actions | Where-Object {
                $_.Arguments -like '*Envoke.vbs*'
            }
        } | Select-Object -First 1
    }
    catch {
        # ScheduledTasks module not available on this system.
        Add-Check -Name 'Task Scheduler' -Status 'Warn' `
            -Message 'ScheduledTasks module not available -- verify Task Scheduler setup manually.'
        $taskFound = 'unavailable'
    }

    if ($taskFound -ne 'unavailable') {
        if ($null -eq $taskFound) {
            Add-Check -Name 'Task Scheduler' -Status 'Fail' `
                -Message 'No scheduled task found running Envoke.vbs. Import the task: schtasks /Create /XML "EnvokeTask.xml" /TN "Envoke"'
        }
        else {
            Add-Check -Name 'Task Scheduler' -Status 'Pass' `
                -Message "Task Scheduler task '$($taskFound.TaskName)' configured correctly."
        }
    }

    # -------------------------------------------------------------------------
    # Area 4: Write permissions
    # -------------------------------------------------------------------------

    # -- Log directory writable
    $logDirectory = Join-Path $env:LOCALAPPDATA 'Envoke\Logs'
    $logDirExists = Test-Path -Path $logDirectory -PathType Container
    if ($logDirExists) {
        Add-Check -Name 'Log directory' -Status 'Pass' `
            -Message "Log directory writable: $logDirectory"
    }
    else {
        try {
            New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
            Add-Check -Name 'Log directory' -Status 'Pass' `
                -Message "Log directory created: $logDirectory"
        }
        catch {
            Add-Check -Name 'Log directory' -Status 'Fail' `
                -Message "Cannot write to log directory: $logDirectory. Create it: New-Item -ItemType Directory -Path '$logDirectory'"
        }
    }

    # -- Registry path accessible
    $registryPath = 'HKCU:\Software\Envoke'
    $regExists = Test-Path -Path $registryPath
    if ($regExists) {
        Add-Check -Name 'Registry path' -Status 'Pass' `
            -Message "Registry path exists: $registryPath"
    }
    else {
        Add-Check -Name 'Registry path' -Status 'Warn' `
            -Message "Registry path $registryPath not yet created -- will be created on first run."
    }

    # -------------------------------------------------------------------------
    # Console summary table
    # -------------------------------------------------------------------------

    Write-EnvkLog -Level 'INFO' -Message '========================================'
    Write-EnvkLog -Level 'INFO' -Message 'Installation Check Summary'
    Write-EnvkLog -Level 'INFO' -Message '========================================'
    Write-EnvkLog -Level 'INFO' -Message ('{0,-30} {1}' -f 'Check', 'Status')
    Write-EnvkLog -Level 'INFO' -Message '----------------------------------------'

    foreach ($r in $results) {
        $level = if ($r.Status -eq 'Fail') { 'ERROR' } elseif ($r.Status -eq 'Warn') { 'WARNING' } else { 'INFO' }
        Write-EnvkLog -Level $level -Message ('{0,-30} {1}' -f $r.Name, $r.Status)
        if ($r.Status -ne 'Pass') {
            Write-EnvkLog -Level $level -Message " -> $($r.Message)"
        }
    }

    Write-EnvkLog -Level 'INFO' -Message '========================================'

    # -------------------------------------------------------------------------
    # Return results array -- unary comma prevents single-element unboxing.
    # -------------------------------------------------------------------------

    return , $results.ToArray()
}