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, Data, Encryption, Hardware Compatibility, File Service) via
          Get-VcfCheckVsanHealthAllSubTests and isolates red/yellow findings via Get-VcfCheckVsanHealthFailingSubTests.

        Evaluation logic (via ConvertTo-VcfCheckVsanOverallStatusLabel):
        - Pass: All vSAN clusters report healthy status ('green', 'passed', or 'info' - vCenter's "nothing wrong" state).
        - 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 SubTasks,
        sorted alphabetically by ClusterName, and constructs HostDetails cards for detailed sub-test visualization. For Warning
        and Fail results, Rows and the Detail message include only the clusters at that status - clusters reporting Pass are
        omitted, and the Detail text says so explicitly.

        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 -Context $Context)

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

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

        $hostDetails = @($healthResults | ForEach-Object {
            [PSCustomObject]@{
                HostName = $_.Cluster.Name
                Status = ConvertTo-VcfCheckVsanOverallStatusLabel -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 { (ConvertTo-VcfCheckVsanOverallStatusLabel -Status $_.OverallHealthStatus) -eq 'Fail' } | Sort-Object -Property ClusterName)
        $warnRows = @($rows | Where-Object { (ConvertTo-VcfCheckVsanOverallStatusLabel -Status $_.OverallHealthStatus) -eq 'Warning' } | Sort-Object -Property ClusterName)

        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 the following cluster(s) with Warning status (clusters reporting Pass are not shown): $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-VcfCheckVsanOverallStatusLabel {

    <#
        .SYNOPSIS
        Normalizes a vSAN OverallHealthStatus value into the check's Pass/Warning/Fail badge vocabulary.

        .DESCRIPTION
        vCenter's vSAN health API returns OverallHealthStatus as a mix of color words ('green', 'yellow',
        'red') and state words ('passed', 'warning', 'failed', 'info'). 'info' means "nothing wrong" - it is
        not a warning - so it maps to 'Pass' alongside 'green'/'passed'. An unrecognized value defaults to
        'Warning' rather than silently rendering as a healthy Pass.

        .PARAMETER Status
        The raw OverallHealthStatus value returned by Test-VsanClusterHealth.

        .OUTPUTS
        [String] 'Pass', 'Warning', or 'Fail'.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $false)] [String]$Status
    )

    if (-not $Status) { return 'Warning' }
    $normalized = $Status.ToLowerInvariant()
    if ($normalized -in @('red', 'failed')) { return 'Fail' }
    if ($normalized -in @('yellow', 'warning')) { return 'Warning' }
    if ($normalized -in @('green', 'passed', 'info')) { return 'Pass' }
    return 'Warning'
}

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

    # NetworkPartition always lists at least one group - the group containing all connected
    # hosts - so a count of 1 means the cluster is unified, not partitioned. Only 2+ groups
    # indicate an actual network split.
    $partitionCount = (ConvertTo-VcfCheckVsanSafeArray -Value $net.NetworkPartition).Count
    $partitionHealth = if ($partitionCount -gt 1) { 'red' } else { 'green' }
    $partitionDescription = if ($partitionCount -gt 1) { "Cluster is split into $partitionCount network partitions" } 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-VcfCheckVsanHealthDataTests {

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

        .DESCRIPTION
        Extracts per-host "Create a new VM" data-health results from a vSAN cluster health object. vCenter's
        OverallHealthStatus rollup includes this test, but it was previously absent from every category function
        below - a cluster could show OverallHealthStatus 'yellow' ("Cluster health issue") while every visible
        sub-test read green, because CreateVMHealth was never surfaced in the Rows/HostDetails tables.

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

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


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

    $rows = [System.Collections.Generic.List[Object]]::new()
    $createVmResults = ConvertTo-VcfCheckVsanSafeArray -Value $HealthResult.CreateVMHealth
    if ($createVmResults.Count -eq 0) {
        return $rows.ToArray()
    }

    $dataHealth = if (@($createVmResults | Where-Object { $_.State -and $_.State -ne 'green' -and $_.State -ne 'passed' }).Count -gt 0) { 'red' } else { 'green' }

    foreach ($createVmResult in $createVmResults) {
        $hostName = if ($createVmResult.Host) { $createVmResult.Host.Name } else { 'Unknown host' }
        $rows.Add((New-VcfCheckVsanHealthTestRow -GroupName 'Data' -GroupHealth $dataHealth -TestName "Create a new VM: $hostName" -TestHealth $createVmResult.State))
    }

    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, Data, 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-VcfCheckVsanHealthDataTests -HealthResult $HealthResult))
    $allTests.AddRange([Object[]]@(Get-VcfCheckVsanHealthMiscTests -HealthResult $HealthResult))

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

function Get-VcfCheckVsanHealthFailingSubTests {

    <#
        .SYNOPSIS
        Extracts red/yellow failing sub-tests from a vSAN cluster health evaluation.

        .DESCRIPTION
        Filters the full set of sub-tests returned by Get-VcfCheckVsanHealthAllSubTests to isolate red/yellow
        findings - explicitly excluding 'N/A' sub-tests that were never applicable rather than treating them as
        failures - formatting the result 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'
    }

    $failingValues = @('red', 'failed', 'yellow', 'warning')
    $failingTests = foreach ($test in $allSubTests) {
        if ($test.TestHealth -and $test.TestHealth.ToLowerInvariant() -in $failingValues) {
            $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 '; ')
}