Private/Checks/Vsan/Test-VcfVsanHealthCheck.ps1

# Copyright (c) 2026 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
#
# =============================================================================
#
# SOFTWARE LICENSE AGREEMENT
#
# Copyright (c) CA, Inc. All rights reserved.
#
# You are hereby granted a non-exclusive, worldwide, royalty-free license
# under CA, Inc.'s copyrights to use, copy, modify, and distribute this
# software in source code or binary form for use in connection with CA, Inc.
# products.
#
# This copyright notice shall be included in all copies or substantial
# portions of the software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# =============================================================================
function Test-VcfVsanHealthCheck {

    <#
        .SYNOPSIS
        Checks overall vSAN cluster health status for every vSAN-enabled cluster attached to a vCenter.

        .DESCRIPTION
        Queries vSAN cluster health across all vCenter appliances connected to SDDC Manager using Get-VcfCheckVsanClusterHealth
        (Test-VsanClusterHealth).

        Evaluates overall cluster health statuses and per-category sub-tests:
        - Evaluates OverallHealthStatus using both color-based ('green', 'yellow', 'red') and state-based ('passed', 'warning', 'failed') status indicators.
        - Captures human-readable diagnostic summaries from OverallHealthDescription.
        - Parses per-category sub-tests (Cluster, Network, Limits, Physical Disk, Encryption, Hardware Compatibility, File Service) via
          Get-VcfCheckVsanHealthAllSubTests and isolates non-green findings via Get-VcfCheckVsanHealthFailingSubTests.

        Evaluation logic:
        - Pass: All vSAN clusters report healthy status ('green' or 'passed').
        - Warning: One or more vSAN clusters report warning status ('yellow' or 'warning') or unrecognized health states.
        - Fail: One or more vSAN clusters report failed status ('red' or 'failed').
        - Skipped: No vSAN-enabled clusters are found in the target vCenter inventory.

        Populates a structured Rows summary table detailing ClusterName, OverallHealthStatus, OverallHealthDescription, and FailingSubTests,
        and constructs HostDetails cards for detailed sub-test visualization.

        Delegates per-vCenter execution and per-domain outcome packaging to Invoke-VcfCheckPerVCenterCheck.

        .PARAMETER Context
        The VcfCheck.Context object. Must already be connected to SDDC Manager.

        .PARAMETER DisplayName
        Optional friendly display name for the check result.

        .OUTPUTS
        [PSObject[]] Per-vCenter check results generated by Invoke-VcfCheckPerVCenterCheck.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $false)] [String]$DisplayName = ''
    )

    return Invoke-VcfCheckPerVCenterCheck -Context $Context -CheckId 'vsan_health_check' -Area vSAN -DisplayName $DisplayName -Body {
        param($Context, $VCenterFqdn)

        $healthResults = @(Get-VcfCheckVsanClusterHealth -Server $VCenterFqdn)

        if ($healthResults.Count -eq 0) {
            return [PSCustomObject]@{ Status = 'Skipped'; Detail = 'No vSAN-enabled cluster found on this vCenter.'; SkipReasonTag = 'no vSAN cluster'; Rows = @() }
        }

        $failValues = @('red', 'failed')
        $warningValues = @('yellow', 'warning')
        $passValues = @('green', 'passed')

        $rows = @($healthResults | ForEach-Object {
            [PSCustomObject]@{
                ClusterName = $_.Cluster.Name
                OverallHealthStatus = $_.OverallHealthStatus
                OverallHealthDescription = $_.OverallHealthDescription
                FailingSubTests = Get-VcfCheckVsanHealthFailingSubTests -HealthResult $_
            }
        })

        $hostDetails = @($healthResults | ForEach-Object {
            [PSCustomObject]@{
                HostName = $_.Cluster.Name
                Status = $_.OverallHealthStatus
                OverallHealthDescription = $_.OverallHealthDescription
                SubTests = @(Get-VcfCheckVsanHealthAllSubTests -HealthResult $_)
            }
        })

        $troubleshootingReference = Get-VcfCheckVsanTroubleshootingKbText -Number '326929' -Url 'https://knowledge.broadcom.com/external/article/326929/vsan-health-service-data-health-vsan-o.html'
        $failRows = @($rows | Where-Object { $_.OverallHealthStatus -and $_.OverallHealthStatus.ToLowerInvariant() -in $failValues })
        $warnRows = @($rows | Where-Object { $_.OverallHealthStatus -and ($_.OverallHealthStatus.ToLowerInvariant() -in $warningValues -or $_.OverallHealthStatus.ToLowerInvariant() -notin ($failValues + $warningValues + $passValues)) })

        if ($failRows.Count -gt 0) {
            $clusterNames = ($failRows.ClusterName) -join '; '
            return [PSCustomObject]@{ Status = 'Fail'; Detail = "vSAN health is unhealthy on: $clusterNames. $troubleshootingReference"; Rows = $failRows; HostDetails = $hostDetails; HostDetailsLabel = 'vSAN Cluster Health Details' }
        }

        if ($warnRows.Count -gt 0) {
            $clusterNames = ($warnRows.ClusterName) -join '; '
            return [PSCustomObject]@{ Status = 'Warning'; Detail = "vSAN health needs attention on: $clusterNames. $troubleshootingReference"; Rows = $warnRows; HostDetails = $hostDetails; HostDetailsLabel = 'vSAN Cluster Health Details' }
        }

        return [PSCustomObject]@{ Status = 'Pass'; Detail = "Checked $($healthResults.Count) vSAN cluster(s); all report healthy."; Rows = $rows; HostDetails = $hostDetails; HostDetailsLabel = 'vSAN Cluster Health Details' }
    }
}

function ConvertTo-VcfCheckVsanFlagHealth {

    <#
        .SYNOPSIS
        Converts a boolean success flag into a standard vSAN health status string.

        .DESCRIPTION
        Maps nullable boolean flags returned by vSAN health queries to standardized 'green' or 'red'
        status indicators used in sub-test results. Returns $null if the input flag is $null.

        .PARAMETER Flag
        The boolean success flag to convert.

        .OUTPUTS
        [String] 'green', 'red', or $null.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $false)] [Nullable[Boolean]]$Flag
    )

    if ($null -eq $Flag) { return $null }
    if ($Flag) { return 'green' } else { return 'red' }
}

function ConvertTo-VcfCheckVsanIssueHealth {

    <#
        .SYNOPSIS
        Converts an IssueFound boolean flag into a standard vSAN health status string.

        .DESCRIPTION
        Maps nullable IssueFound boolean flags returned by vSAN health queries to standardized 'green'
        or 'red' status indicators (where $true converts to 'red' and $false converts to 'green').
        Returns $null if the input flag is $null.

        .PARAMETER IssueFound
        The IssueFound flag to convert.

        .OUTPUTS
        [String] 'green', 'red', or $null.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $false)] [Nullable[Boolean]]$IssueFound
    )

    if ($null -eq $IssueFound) { return $null }
    if ($IssueFound) { return 'red' } else { return 'green' }
}

function ConvertTo-VcfCheckVsanSafeArray {

    <#
        .SYNOPSIS
        Safely wraps array properties from vSAN health responses.

        .DESCRIPTION
        Ensures null or unpopulated array properties from vSAN health API responses evaluate to an empty
        array rather than a single $null element.

        .PARAMETER Value
        The array property value to evaluate and wrap.

        .OUTPUTS
        [Object[]] Empty array if input is $null; otherwise an array wrapping the input value.
    #>


    [CmdletBinding()]
    [OutputType([Object[]])]
    Param (
        [Parameter(Mandatory = $false)] [PSObject]$Value
    )

    if ($null -eq $Value) { return @() }
    return @($Value)
}

function New-VcfCheckVsanHealthTestRow {

    <#
        .SYNOPSIS
        Constructs a structured sub-test report object for vSAN cluster health details.

        .DESCRIPTION
        Creates a custom object representing an individual vSAN health sub-test result, containing GroupName,
        GroupHealth, TestName, TestHealth, and TestDescription properties.

        .PARAMETER GroupName
        The vSAN health category name (e.g., 'Network', 'Limits').

        .PARAMETER GroupHealth
        The rollup health status for the category.

        .PARAMETER TestName
        The display name of the sub-test.

        .PARAMETER TestHealth
        The health status value for the sub-test (defaults to 'N/A' if empty).

        .PARAMETER TestDescription
        Optional descriptive details for the sub-test outcome.

        .OUTPUTS
        [PSCustomObject] Structured sub-test report row object.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [String]$GroupName,
        [Parameter(Mandatory = $false)] [String]$GroupHealth,
        [Parameter(Mandatory = $true)] [String]$TestName,
        [Parameter(Mandatory = $false)] [String]$TestHealth,
        [Parameter(Mandatory = $false)] [String]$TestDescription
    )

    return [PSCustomObject]@{
        GroupName = $GroupName
        GroupHealth = $GroupHealth
        TestName = $TestName
        TestHealth = if ($TestHealth) { $TestHealth } else { 'N/A' }
        TestDescription = $TestDescription
    }
}

function Get-VcfCheckVsanHealthClusterTests {

    <#
        .SYNOPSIS
        Builds sub-test report rows for the Cluster vSAN health category.

        .DESCRIPTION
        Extracts cluster-level health indicators from a vSAN cluster health object, including overall health findings,
        software version compatibility, daemon liveness, and advanced configuration synchronization state.

        .PARAMETER HealthResult
        A vSAN cluster health result object returned by Test-VsanClusterHealth.

        .OUTPUTS
        [PSObject[]] Array of sub-test report rows for the Cluster category.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$HealthResult
    )

    $rows = [System.Collections.Generic.List[Object]]::new()

    if ($HealthResult.HealthSystemStatus) {
        $status = $HealthResult.HealthSystemStatus.Status
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Cluster' -GroupHealth $status -TestName 'Overall health findings' -TestHealth $status))
    }
    if ($HealthResult.HealthSystemVersion) {
        $versionHealth = ConvertTo-VcfCheckVsanIssueHealth -IssueFound $HealthResult.HealthSystemVersion.IssueFound
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Cluster' -GroupHealth $versionHealth -TestName 'Software version compatibility' -TestHealth $versionHealth -TestDescription $HealthResult.HealthSystemVersion.VsanVersion))
    }
    if ($HealthResult.ClomdLiveness) {
        $clomdHealth = ConvertTo-VcfCheckVsanIssueHealth -IssueFound $HealthResult.ClomdLiveness.IssueFound
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Cluster' -GroupHealth $clomdHealth -TestName 'vSAN daemon liveness' -TestHealth $clomdHealth))
    }
    if ('AdvancedConfigInSync' -in $HealthResult.PSObject.Properties.Name) {
        # $null here means "no mismatches found" (healthy), not "not applicable" - unlike the
        # other categories, so this checks the property's existence rather than its truthiness
        # to avoid fabricating a green result for an object that never had this field.
        $advancedConfigHealth = if ((ConvertTo-VcfCheckVsanSafeArray -Value $HealthResult.AdvancedConfigInSync).Count -gt 0) { 'red' } else { 'green' }
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Cluster' -GroupHealth $advancedConfigHealth -TestName 'Advanced vSAN configuration in sync' -TestHealth $advancedConfigHealth))
    }

    return $rows.ToArray()
}

function Get-VcfCheckVsanHealthNetworkTests {

    <#
        .SYNOPSIS
        Builds sub-test report rows for the Network vSAN health category.

        .DESCRIPTION
        Extracts network health indicators from a vSAN cluster health object, including unicast ping tests,
        MTU packet size tests, vmknic presence, subnet matching, multicast configuration, partition status,
        and host communication status.

        .PARAMETER HealthResult
        A vSAN cluster health result object returned by Test-VsanClusterHealth.

        .OUTPUTS
        [PSObject[]] Array of sub-test report rows for the Network category.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$HealthResult
    )

    $rows = [System.Collections.Generic.List[Object]]::new()
    if (-not $HealthResult.NetworkHealth) {
        return $rows.ToArray()
    }

    $net = $HealthResult.NetworkHealth
    $pingHealth = ConvertTo-VcfCheckVsanFlagHealth -Flag $net.PingTestSuccess
    $largePingHealth = ConvertTo-VcfCheckVsanFlagHealth -Flag $net.LargePingTestSuccess
    $vmknicHealth = ConvertTo-VcfCheckVsanFlagHealth -Flag $net.VsanVmknicPresent
    $subnetHealth = ConvertTo-VcfCheckVsanFlagHealth -Flag $net.MatchingIPSubnets
    $multicastHealth = ConvertTo-VcfCheckVsanFlagHealth -Flag $net.MatchingMulticastConfig

    $partitionCount = (ConvertTo-VcfCheckVsanSafeArray -Value $net.NetworkPartition).Count
    $partitionHealth = if ($partitionCount -gt 0) { 'red' } else { 'green' }
    $partitionDescription = if ($partitionCount -gt 0) { "Cluster is partitioned into $partitionCount group(s)" } else { $null }

    $connectivityIssueHostCount = (ConvertTo-VcfCheckVsanSafeArray -Value $net.HostDisconnected).Count + (ConvertTo-VcfCheckVsanSafeArray -Value $net.HostCommunicationFailure).Count
    $connectivityHealth = if ($connectivityIssueHostCount -gt 0) { 'red' } else { 'green' }

    # GroupHealth is derived as the worst of this category's own sub-tests, not vCenter's
    # separate net.IssueFound rollup - that flag doesn't always agree with the sub-tests below
    # (e.g. a 1-host partition it doesn't count as "IssueFound"), which let GroupHealth show
    # green next to a red TestHealth in the same row.
    $netHealth = if (@($pingHealth, $largePingHealth, $vmknicHealth, $subnetHealth, $multicastHealth, $partitionHealth, $connectivityHealth) -contains 'red') { 'red' } else { 'green' }

    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Network' -GroupHealth $netHealth -TestName 'vSAN: Basic (unicast) connectivity check' -TestHealth $pingHealth))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Network' -GroupHealth $netHealth -TestName 'vSAN: MTU check (ping with large packet size)' -TestHealth $largePingHealth))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Network' -GroupHealth $netHealth -TestName 'All hosts have a vSAN vmknic configured' -TestHealth $vmknicHealth))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Network' -GroupHealth $netHealth -TestName 'All hosts have matching subnets' -TestHealth $subnetHealth))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Network' -GroupHealth $netHealth -TestName 'Multicast configuration consistent' -TestHealth $multicastHealth))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Network' -GroupHealth $netHealth -TestName 'vSAN cluster partition' -TestHealth $partitionHealth -TestDescription $partitionDescription))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Network' -GroupHealth $netHealth -TestName 'Hosts with connectivity issues' -TestHealth $connectivityHealth))

    return $rows.ToArray()
}

function Get-VcfCheckVsanHealthLimitTests {

    <#
        .SYNOPSIS
        Builds sub-test report rows for the Limits vSAN health category.

        .DESCRIPTION
        Extracts resource limit health indicators from a vSAN cluster health object, including component limits,
        disk free space, and read cache reservations.

        .PARAMETER HealthResult
        A vSAN cluster health result object returned by Test-VsanClusterHealth.

        .OUTPUTS
        [PSObject[]] Array of sub-test report rows for the Limits category.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$HealthResult
    )

    $rows = [System.Collections.Generic.List[Object]]::new()
    if (-not $HealthResult.LimitHealth) {
        return $rows.ToArray()
    }

    $limit = $HealthResult.LimitHealth
    $limitHealth = ConvertTo-VcfCheckVsanIssueHealth -IssueFound $limit.IssueFound
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Limits' -GroupHealth $limitHealth -TestName 'Current cluster situation' -TestHealth $limit.ComponentLimitHealth))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Limits' -GroupHealth $limitHealth -TestName 'Disk space' -TestHealth $limit.DiskFreeSpaceHealth))
    $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Limits' -GroupHealth $limitHealth -TestName 'Read cache reservations' -TestHealth $limit.ReadCacheFreeReservationHealth))

    return $rows.ToArray()
}

function Get-VcfCheckVsanHealthPhysicalDiskTests {

    <#
        .SYNOPSIS
        Builds sub-test report rows for the Physical Disk vSAN health category.

        .DESCRIPTION
        Extracts physical disk health indicators from a vSAN cluster health object, including vSAN disk balance
        and per-host physical disk health statuses.

        .PARAMETER HealthResult
        A vSAN cluster health result object returned by Test-VsanClusterHealth.

        .OUTPUTS
        [PSObject[]] Array of sub-test report rows for the Physical Disk category.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$HealthResult
    )

    $rows = [System.Collections.Generic.List[Object]]::new()

    $diskBalance = ConvertTo-VcfCheckVsanSafeArray -Value $HealthResult.DiskBalance
    $diskBalanceHealth = $null
    if ($diskBalance.Count -gt 0) {
        $imbalancedCount = @($diskBalance | Where-Object { $_.UsageAboveThreshold -gt 0 }).Count
        $diskBalanceHealth = if ($imbalancedCount -gt 0) { 'yellow' } else { 'green' }
    }

    $diskHealthResult = ConvertTo-VcfCheckVsanSafeArray -Value $HealthResult.DiskHealthResult

    # GroupHealth is the worst of the disk balance result and every per-host disk health below -
    # there is no separate "Physical Disk" rollup field on the health result to read instead, and
    # leaving GroupHealth unset here (as before) left it blank on every per-host row.
    $perHostHealthValues = @($diskHealthResult | ForEach-Object { $_.OverallHealth })
    $physicalDiskHealth = if (@($diskBalanceHealth) + $perHostHealthValues -contains 'red') { 'red' }
        elseif (@($diskBalanceHealth) + $perHostHealthValues -contains 'yellow') { 'yellow' }
        else { 'green' }

    if ($diskBalance.Count -gt 0) {
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Physical Disk' -GroupHealth $physicalDiskHealth -TestName 'vSAN Disk Balance' -TestHealth $diskBalanceHealth -TestDescription "$imbalancedCount of $($diskBalance.Count) disk(s) above balance threshold"))
    }

    foreach ($perHostDiskHealth in $diskHealthResult) {
        $diskHostName = if ($perHostDiskHealth.Host) { $perHostDiskHealth.Host.Name } else { 'Unknown host' }
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Physical Disk' -GroupHealth $physicalDiskHealth -TestName "Disk health: $diskHostName" -TestHealth $perHostDiskHealth.OverallHealth -TestDescription $perHostDiskHealth.Error))
    }

    return $rows.ToArray()
}

function Get-VcfCheckVsanHealthMiscTests {

    <#
        .SYNOPSIS
        Builds sub-test report rows for Encryption, Hardware Compatibility, and File Service vSAN health categories.

        .DESCRIPTION
        Extracts health indicators for Encryption (configuration and KMS cluster health), Hardware Compatibility
        (HCL database age), and File Service overall health from a vSAN cluster health object.

        .PARAMETER HealthResult
        A vSAN cluster health result object returned by Test-VsanClusterHealth.

        .OUTPUTS
        [PSObject[]] Array of sub-test report rows for Encryption, Hardware Compatibility, and File Service categories.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$HealthResult
    )

    $rows = [System.Collections.Generic.List[Object]]::new()

    if ($HealthResult.EncryptionHealth) {
        $encryption = $HealthResult.EncryptionHealth
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Encryption' -GroupHealth $encryption.OverallHealth -TestName 'Encryption configuration' -TestHealth $encryption.ConfigurationHealth))
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Encryption' -GroupHealth $encryption.OverallHealth -TestName 'KMS cluster health' -TestHealth $encryption.OverallKmsHealth))
    }

    if ($HealthResult.HclInfo) {
        $hcl = $HealthResult.HclInfo
        $hclDescription = if ($hcl.LastUpdated) { "Last updated: $($hcl.LastUpdated)" } else { $null }
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Hardware Compatibility' -GroupHealth $hcl.DatabaseAgeHealth -TestName 'HCL database up to date' -TestHealth $hcl.DatabaseAgeHealth -TestDescription $hclDescription))
    }

    if ($HealthResult.FileServiceHealth -and 'OverallHealth' -in $HealthResult.FileServiceHealth.PSObject.Properties.Name) {
        $fileServiceHealth = $HealthResult.FileServiceHealth.OverallHealth
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'File Service' -GroupHealth $fileServiceHealth -TestName 'File service health' -TestHealth $fileServiceHealth))
    }

    return $rows.ToArray()
}

function Sort-VcfCheckVsanHealthTestsBySeverity {

    <#
        .SYNOPSIS
        Sorts vSAN health sub-test report rows by health severity.

        .DESCRIPTION
        Orders sub-test report rows so that unhealthy or warning items ('red', 'failed', 'yellow', 'warning')
        appear ahead of healthy items ('green', 'passed'), preserving logical grouping and scannability.

        .PARAMETER Tests
        Array of sub-test report row objects to sort.

        .OUTPUTS
        [PSObject[]] Sorted array of sub-test report rows.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $false)] [AllowEmptyCollection()] [PSObject[]]$Tests
    )

    $severityRank = @{ 'red' = 0; 'failed' = 0; 'yellow' = 1; 'warning' = 1; 'green' = 3; 'passed' = 3 }
    $rankedTests = foreach ($test in $Tests) {
        $rank = if ($test.TestHealth -ne 'N/A' -and $severityRank.ContainsKey($test.TestHealth.ToLowerInvariant())) { $severityRank[$test.TestHealth.ToLowerInvariant()] } else { 2 }
        $test | Add-Member -MemberType NoteProperty -Name SeverityRank -Value $rank -PassThru
    }

    return @(@($rankedTests) | Sort-Object -Property SeverityRank -Stable | Select-Object -Property GroupName, GroupHealth, TestName, TestHealth, TestDescription)
}

function Get-VcfCheckVsanHealthAllSubTests {

    <#
        .SYNOPSIS
        Retrieves all sub-test results across categories for a vSAN cluster health evaluation.

        .DESCRIPTION
        Aggregates sub-tests across Cluster, Network, Limits, Physical Disk, Encryption, Hardware Compatibility,
        and File Service categories from a vSAN cluster health object, returning a severity-sorted list of all sub-tests.

        .PARAMETER HealthResult
        A vSAN cluster health result object returned by Test-VsanClusterHealth.

        .OUTPUTS
        [PSObject[]] Severity-sorted array of all sub-test report rows for the cluster.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$HealthResult
    )

    $allTests = [System.Collections.Generic.List[Object]]::new()
    $allTests.AddRange([Object[]]@(Get-VcfCheckVsanHealthClusterTests -HealthResult $HealthResult))
    $allTests.AddRange([Object[]]@(Get-VcfCheckVsanHealthNetworkTests -HealthResult $HealthResult))
    $allTests.AddRange([Object[]]@(Get-VcfCheckVsanHealthLimitTests -HealthResult $HealthResult))
    $allTests.AddRange([Object[]]@(Get-VcfCheckVsanHealthPhysicalDiskTests -HealthResult $HealthResult))
    $allTests.AddRange([Object[]]@(Get-VcfCheckVsanHealthMiscTests -HealthResult $HealthResult))

    return Sort-VcfCheckVsanHealthTestsBySeverity -Tests $allTests.ToArray()
}

function Get-VcfCheckVsanHealthFailingSubTests {

    <#
        .SYNOPSIS
        Extracts non-green failing sub-tests from a vSAN cluster health evaluation.

        .DESCRIPTION
        Filters the full set of sub-tests returned by Get-VcfCheckVsanHealthAllSubTests to isolate non-green findings,
        formatting them as a semicolon-separated summary string. Returns 'N/A' if no failing sub-tests are detected.

        .PARAMETER HealthResult
        A vSAN cluster health result object returned by Test-VsanClusterHealth.

        .OUTPUTS
        [String] Semicolon-delimited summary string of failing sub-tests, or 'N/A'.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$HealthResult
    )

    $allSubTests = @(Get-VcfCheckVsanHealthAllSubTests -HealthResult $HealthResult)
    if ($allSubTests.Count -eq 0) {
        return 'N/A'
    }

    $healthyValues = @('green', 'passed')
    $failingTests = foreach ($test in $allSubTests) {
        if ($test.TestHealth -and $test.TestHealth.ToLowerInvariant() -notin $healthyValues) {
            $description = if ($test.TestDescription) { $test.TestDescription } else { $test.TestName }
            "$($test.GroupName): $($test.TestName) - $description"
        }
    }

    if (@($failingTests).Count -eq 0) { return 'N/A' }
    return ($failingTests -join '; ')
}