DeliveryOptimizationTroubleshooter.ps1

<#PSScriptInfo
 
.VERSION 1.2.0
 
.GUID 9516d007-5e02-4bfd-84a4-436ea6778687
 
.AUTHOR carmenf
 
.COMPANYNAME Microsoft Corporation
 
.COPYRIGHT
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
2024-04-04 v1.2.0
    Introduce MCC related checks.
    Reorganize output data.
 
.PRIVATEDATA
 
#>


<#
.DESCRIPTION
 Troubleshoot Delivery Optimization by performing device health checks and peer-to-peer configuration of the device. This PowerShell script is officially signed by Microsoft.
#>


<#
    .SYNOPSIS
        Script for:
        - Checking Device, Network, DO, P2P and MCC Settings.
 
    .PARAMETER HealthCheck
        A Health Checker script that displays settings to help the user to validate if there are any wrong settings in the user device, network, DO.
 
    .PARAMETER P2P
        Show to user the P2P efficiency of the device, errors found and Policy settings.
 
    .PARAMETER MCC
        Show MCC settings to allow customers to ensure the Windows device can correctly connect to the CacheHost server on the network, for supported content downloads.
 
    .EXAMPLE
        To run all script verifications
 
            DeliveryOptimizationTroubleshooter.ps1
 
    .EXAMPLE
        To run only Healthcheck
 
            DeliveryOptimizationTroubleshooter.ps1 -HealthCheck
 
    .EXAMPLE
        To run only P2P validation
 
            DeliveryOptimizationTroubleshooter.ps1 -P2P
 
    .EXAMPLE
        To run only MCC validation
 
            DeliveryOptimizationTroubleshooter.ps1 -MCC
#>


[CmdLetBinding()]
Param(
    [switch] $HealthCheck,
    [switch] $P2P,
    [switch] $MCC
)

#----------------------------------------------------------------------------------#
# Enums
Add-Type -TypeDefinition @"
    public enum TestResult
    {
        Unset,
        Fail,
        Pass,
        Disabled,
        Warn,
    }
"@


Add-Type -TypeDefinition @"
    public enum CacheHostSource
    {
        DisableDNSSD = 0,
        DHCPOption235 = 1,
        DHCPOption235Force = 2,
    }
"@


enum DhcpOption
{
    DOGroupId  = 234
    DOMccHost  = 235
}

#----------------------------------------------------------------------------------#
# Get Custom Objects
function Get-DOErrorsTable(){
    $errorsObj = @'
[
    {
        "ErrorCode": "0x80D01001",
        "Description": "Delivery Optimization was unable to provide the service."
    },
    {
        "ErrorCode": "0x80D02002",
        "Description": "Download of a file saw no progress within the defined period."
    },
    {
        "ErrorCode": "0x80D02003",
        "Description": "Job was not found."
    },
    {
        "ErrorCode": "0x80D02004",
        "Description": "There were no files in the job."
    },
    {
        "ErrorCode": "0x80D02005",
        "Description": "No downloads currently exist."
    },
    {
        "ErrorCode": "0x80D0200B",
        "Description": "Memory stream transfer is not supported."
    },
    {
        "ErrorCode": "0x80D0200C",
        "Description": "Job has neither completed nor has it been cancelled prior to reaching the max age threshold."
    },
    {
        "ErrorCode": "0x80D0200D",
        "Description": "There is no local file path specified for this download."
    },
    {
        "ErrorCode": "0x80D02010",
        "Description": "No file is available because no URL generated an error."
    },
    {
        "ErrorCode": "0x80D02011",
        "Description": "SetProperty() or GetProperty() called with an unknown property ID."
    },
    {
        "ErrorCode": "0x80D02012",
        "Description": "Unable to call SetProperty() on a read-only property."
    },
    {
        "ErrorCode": "0x80D02013",
        "Description": "The requested action is not allowed in the current job state."
    },
    {
        "ErrorCode": "0x80D02015",
        "Description": "Unable to call GetProperty() on a write-only property."
    },
    {
        "ErrorCode": "0x80D02016",
        "Description": "Download job is marked as requiring integrity checking but integrity checking info was not specified."
    },
    {
        "ErrorCode": "0x80D02017",
        "Description": "Download job is marked as requiring integrity checking but integrity checking info could not be retrieved."
    },
    {
        "ErrorCode": "0x80D02018",
        "Description": "Unable to start a download because no download sink (either local file or stream interface) was specified."
    },
    {
        "ErrorCode": "0x80D02019",
        "Description": "An attempt to set a download sink failed because another type of sink is already set."
    },
    {
        "ErrorCode": "0x80D0201A",
        "Description": "Unable to determine file size from HTTP 200 status code."
    },
    {
        "ErrorCode": "0x80D0201B",
        "Description": "Decryption key was provided but file on CDN does not appear to be encrypted."
    },
    {
        "ErrorCode": "0x80D0201C",
        "Description": "Unable to determine file size from HTTP 206 status code."
    },
    {
        "ErrorCode": "0x80D0201D",
        "Description": "Unable to determine file size from an unexpected HTTP 2xx status code."
    },
    {
        "ErrorCode": "0x80D0201E",
        "Description": "User consent to access the network is required to proceed."
    },
    {
        "ErrorCode": "0x80D02200",
        "Description": "The download was started without providing a URI."
    },
    {
        "ErrorCode": "0x80D02201",
        "Description": "The download was started without providing a content ID."
    },
    {
        "ErrorCode": "0x80D02202",
        "Description": "The specified content ID is invalid."
    },
    {
        "ErrorCode": "0x80D02203",
        "Description": "Ranges are unexpected for the current download."
    },
    {
        "ErrorCode": "0x80D02204",
        "Description": "Ranges are expected for the current download."
    },
    {
        "ErrorCode": "0x80D03001",
        "Description": "Download job not allowed due to participation throttling."
    },
    {
        "ErrorCode": "0x80D03002",
        "Description": "Download job not allowed due to user/admin settings."
    },
    {
        "ErrorCode": "0x80D03801",
        "Description": "DO core paused the job due to cost policy restrictions."
    },
    {
        "ErrorCode": "0x80D03802",
        "Description": "DO job download mode restricted by content policy."
    },
    {
        "ErrorCode": "0x80D03803",
        "Description": "DO core paused the job due to detection of cellular network and policy restrictions."
    },
    {
        "ErrorCode": "0x80D03804",
        "Description": "DO core paused the job due to detection of power state change into non-AC mode.",
        "RelatedPolicyName": "DOMinBatteryPercentageAllowedToUpload",
        "SuggestedRemedy": "Please check your Battery level is enough to P2P."
    },
    {
        "ErrorCode": "0x80D03805",
        "Description": "DO core paused the job due to loss of network connectivity."
    },
    {
        "ErrorCode": "0x80D03806",
        "Description": "DO job download mode restricted by policy."
    },
    {
        "ErrorCode": "0x80D03807",
        "Description": "DO core paused the completed job due to detection of VPN network.",
        "RelatedPolicyName": "DOAllowVPNPeerCaching",
        "SuggestedRemedy": "Check you are connected to any VPN when you are doing P2P."
    },
    {
        "ErrorCode": "0x80D03808",
        "Description": "DO core paused the completed job due to detection of critical memory usage on the system."
    },
    {
        "ErrorCode": "0x80D03809",
        "Description": "DO job download mode restricted due to absence of the cache folder."
    },
    {
        "ErrorCode": "0x80D0380A",
        "Description": "Unable to contact one or more DO cloud services."
    },
    {
        "ErrorCode": "0x80D0380B",
        "Description": "DO job download mode restricted for unregistered caller."
    },
    {
        "ErrorCode": "0x80D0380C",
        "Description": "DO job is using the simple ranges download in simple mode."
    },
    {
        "ErrorCode": "0x80D0380D",
        "Description": "DO job paused due to unexpected HTTP response codes (e.g. 204)."
    },
    {
        "ErrorCode": "0x80D05001",
        "Description": "HTTP server returned a response with data size not equal to what was requested."
    },
    {
        "ErrorCode": "0x80D05002",
        "Description": "The Http server certificate validation has failed."
    },
    {
        "ErrorCode": "0x80D05010",
        "Description": "The specified byte range is invalid."
    },
    {
        "ErrorCode": "0x80D05011",
        "Description": "The server does not support the necessary HTTP protocol. Delivery Optimization (DO) requires that the server support the Range protocol header."
    },
    {
        "ErrorCode": "0x80D05012",
        "Description": "The list of byte ranges contains some overlapping ranges, which are not supported."
    },
    {
        "ErrorCode": "0x80D06800",
        "Description": "Too many bad pieces found during upload."
    },
    {
        "ErrorCode": "0x80D06802",
        "Description": "Fatal error encountered in core."
    },
    {
        "ErrorCode": "0x80D06803",
        "Description": "Services response was an empty JSON content."
    },
    {
        "ErrorCode": "0x80D06804",
        "Description": "Received bad or incomplete data for a content piece."
    },
    {
        "ErrorCode": "0x80D06805",
        "Description": "Content piece hash check failed."
    },
    {
        "ErrorCode": "0x80D06806",
        "Description": "Content piece hash check failed but source is not banned yet."
    },
    {
        "ErrorCode": "0x80D06807",
        "Description": "The piece was rejected because it already exists in the cache."
    },
    {
        "ErrorCode": "0x80D06808",
        "Description": "The piece requested is no longer available in the cache."
    },
    {
        "ErrorCode": "0x80D06809",
        "Description": "Invalid metainfo content."
    },
    {
        "ErrorCode": "0x80D0680A",
        "Description": "Invalid metainfo version."
    },
    {
        "ErrorCode": "0x80D0680B",
        "Description": "The swarm isn't running."
    },
    {
        "ErrorCode": "0x80D0680C",
        "Description": "The peer was not recognized by the connection manager."
    },
    {
        "ErrorCode": "0x80D0680D",
        "Description": "The peer is banned."
    },
    {
        "ErrorCode": "0x80D0680E",
        "Description": "The client is trying to connect to itself."
    },
    {
        "ErrorCode": "0x80D0680F",
        "Description": "The socket or peer is already connected."
    },
    {
        "ErrorCode": "0x80D06810",
        "Description": "The maximum number of connections has been reached."
    },
    {
        "ErrorCode": "0x80D06811",
        "Description": "The connection was lost."
    },
    {
        "ErrorCode": "0x80D06812",
        "Description": "The swarm ID is not recognized."
    },
    {
        "ErrorCode": "0x80D06813",
        "Description": "The handshake length is invalid."
    },
    {
        "ErrorCode": "0x80D06814",
        "Description": "The socket has been closed."
    },
    {
        "ErrorCode": "0x80D06815",
        "Description": "The message is too long."
    },
    {
        "ErrorCode": "0x80D06816",
        "Description": "The message is invalid."
    },
    {
        "ErrorCode": "0x80D06817",
        "Description": "The peer is an upload."
    },
    {
        "ErrorCode": "0x80D06818",
        "Description": "Cannot pin a swarm because it's not in peering mode."
    },
    {
        "ErrorCode": "0x80D06819",
        "Description": "Cannot delete a pinned swarm without using the 'force' flag."
    }
]
'@
 | ConvertFrom-Json

    foreach ($obj in $errorsObj)
    {
        $intValue = [Convert]::ToInt32($obj.ErrorCode, 16)
        $obj.ErrorCode = $intValue
    }

    return $errorsObj
}

function Get-DOPolicyTable(){
    $linkBase = "https://learn.microsoft.com/windows/deployment/do/waas-delivery-optimization-reference"

@"
    [
        {
            "PolicyCode": "DODownloadMode",
            "PolicyName": "Download Mode Configured",
            "Description": "The download method that DO can use in downloads.",
            "Link": "$linkBase#download-mode"
        },
        {
            "PolicyCode": "DOGroupId",
            "PolicyName": "Group ID",
            "Description": "Unique GUID group id to create a custom group.",
            "Link": "$linkBase#group-id"
        },
        {
            "PolicyCode": "DOGroupIdSource",
            "PolicyName": "Group ID Source",
            "Description": "Restrict peer selection to a specific source.",
            "Link": "$linkBase#select-the-source-of-group-ids"
        },
        {
            "PolicyCode": "DORestrictPeerSelectionBy",
            "PolicyName": "Restrict Peer Selection",
            "Description": "Restriction to set peering boundary.",
            "Link": "$linkBase#select-a-method-to-restrict-peer-selection"
        },
        {
            "PolicyCode": "DODelayForegroundDownloadFromHttp",
            "PolicyName": "Delay Foreground from Http",
            "Description": "Control the time to wait for peering (foreground).",
            "Link": "$linkBase#delay-foreground-download-from-http-in-secs"
        },
        {
            "PolicyCode": "DODelayBackgroundDownloadFromHttp",
            "PolicyName": "Delay Background from Http",
            "Description": "Control the time to wait for peering (background).",
            "Link": "$linkBase#delay-background-download-from-http-in-secs"
        },
        {
            "PolicyCode": "DOAllowVPNPeerCaching",
            "PolicyName": "Enable Peering on VPN",
            "Description": "Allow device to use peering while connected to a VPN.",
            "Link": "$linkBase#enable-peer-caching-while-the-device-connects-via-vpn"
        },
        {
            "PolicyCode": "DOMaxCacheAge",
            "PolicyName": "Max Cache Age",
            "PolicyUnit": "(secs)",
            "Description": "Max number of seconds a file can be held in DO cache.",
            "Link": "$linkBase#max-cache-age"
        },
        {
            "PolicyCode": "DOMaxCacheSize",
            "PolicyName": "Max Cache Size",
            "PolicyUnit": "%",
            "Description": "Percentage of available disk drive space allowed​.",
            "Link": "$linkBase#max-cache-size"
        },
        {
            "PolicyCode": "DOAbsoluteMaxCacheSize",
            "PolicyName": "Absolute Max Cache Size",
            "PolicyUnit": "GB",
            "Description": "Max number of gigabytes the DO cache can use.",
            "Link": "$linkBase#absolute-max-cache-size"
        },
        {
            "PolicyCode": "DOMinBatteryPercentageAllowedToUpload",
            "PolicyName": "Allow P2P on Battery",
            "PolicyUnit": "%",
            "Description": "Specifies battery level to allow upload data.",
            "Link": "$linkBase#allow-uploads-while-the-device-is-on-battery-while-under-set-battery-level"
        },
        {
            "PolicyCode": "DOMinDiskSizeAllowedToPeer",
            "PolicyName": "Minimum Free Disk Size",
            "PolicyUnit": "GB",
            "Description": "Required minimum disk size to allow peer caching.",
            "Link": "$linkBase#minimum-disk-size-allowed-to-use-peer-caching"
        },
        {
            "PolicyCode": "DOMinFileSizeToCache",
            "PolicyName": "Minimum Peer File Size",
            "PolicyUnit": "MB",
            "Description": "Minimum content file size to use peer caching.",
            "Link": "$linkBase#minimum-peer-caching-content-file-size"
        },
        {
            "PolicyCode": "DOMinRAMAllowedToPeer",
            "PolicyName": "Minimum RAM size",
            "PolicyUnit": "GB",
            "Description": "Minimum RAM size to use peer caching.",
            "Link": "$linkBase#minimum-ram-inclusive-allowed-to-use-peer-caching"
        },
        {
            "PolicyCode": "DOMinBackgroundQoS",
            "PolicyName": "Minimum Background QoS",
            "PolicyUnit": "KB/s",
            "Description": "Specifies the minimum download speed guarantee.",
            "Link": "$linkBase#minimum-background-qos"
        }
    ]
"@
 | ConvertFrom-Json

}

#----------------------------------------------------------------------------------#
# Print Functions
function Print-OSInfo()
{
    # Check OS Version
    Write-Host "`nWindows" $(Get-OSVersion) -NoNewline

    switch ($os.BuildNumber)
    {
        "10240" { Write-Host " - TH1" }
        "10586" { Write-Host " - TH2" }
        "14393" { Write-Host " - RS1" }
        "15063" { Write-Host " - RS2" }
        "16299" { Write-Host " - RS3" }
        "17134" { Write-Host " - RS4" }
        "17763" { Write-Host " - RS5" }
        "18362" { Write-Host " - Titanium 19H1" }
        "18363" { Write-Host " - Vanadium 19H2" }
        "19041" { Write-Host " - Vibranium 20H1" }
        "19042" { Write-Host " - Vibranium (v2) 20H2" }
        "19645" { Write-Host " - Manganese" }
        "19043" { Write-Host " - Vibranium (v3) 21H1" }
        "19044" { Write-Host " - Vibranium (v4) 21H2" }
        "20348" { Write-Host " - Iron" }
        "22000" { Write-Host " - Cobalt" }
        "22621" { Write-Host " - Nickel" }
        default { Write-Host "" }
    }

    # Check UUS Version
    $uusVerPath = "$env:ProgramData\Microsoft\Windows\UUS\State\_active.uusver"
    if (Test-Path $uusVerPath)
    {
        $uusVersion = Get-Content $uusVerPath
        Write-Host "UUS" $uusVersion
    }

    $PSVersion = "PS Version $($PSVersionTable.PSVersion)"
    Write-Verbose $PSVersion
}

function Print-Title([string] $TextTitle)
{
    Write-Host ("{0}`n{1}`n{0}" -f ('-' * 80), $TextTitle.ToUpper())
}

function Print-SubTitle([string] $TextSubTitle)
{
    Write-Host ("--> {0}:`n{1}" -f $TextSubTitle, ('-' * 55))
}

function Format-ResultObject([pscustomobject] $Object)
{
    $Object | Format-Table -Wrap -Property  @{ Label = "Name"; Expression={ $_.Name }; Align='left'; Width = 30; },
                                            @{ Label = "Result"; Expression={ switch ($_.Result)
                                                                                {
                                                                                    "Fail" { $color = "91"; break }
                                                                                    "Pass" { $color = "92"; break }
                                                                                    "Warn" { $color = "93"; break }
                                                                                    "Disabled" { $color = "93"; break }
                                                                                    default { $color = "37" }
                                                                                }
                                                                                $text = $_.Result.ToString().ToUpper()
                                                                                ; $e = [char]27
                                                                                ;"$e[${color}m$($text)${e}[0m"
                                                                            }; Align='center'; Width = 10;},
                                            @{ Label = "Details"; Expression={ "$($_.Details) `n" }; Align='left'; }
}

#----------------------------------------------------------------------------------#
# Device Check

function Check-AdminPrivileges([string] $InvocationLine)
{
    if (IsElevated)
    {
        return $true;
    }

    $ScriptPath = $MyInvocation.PSCommandPath

    # The new process can't resolve working dir when script is launched like .\dolog.ps1, so we have to parse
    # and rebuild the full script path and param list.
    $scriptParams = ""
    $firstParam = $InvocationLine.IndexOf('-')

    if($firstParam -gt 0)
    {
        $scriptParams = $InvocationLine.Substring($firstParam-1)
    }

    $scriptCmd = "$ScriptPath $scriptParams"

    $arg = "-NoExit -Command `"$scriptCmd`""

    #Check Powershell version to use the right path
    if ($PSVersionTable.PSVersion.Major -lt 7)
    {
        $PSPath = "powershell.exe"
    }
    else
    {
        $PSPath = "pwsh.exe"
    }

    $proc = Start-Process $PSPath -ArgumentList $arg -Verb Runas -ErrorAction Stop

    return $false
}

function IsElevated
{
    $wid = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $prp = new-object System.Security.Principal.WindowsPrincipal($wid)
    $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator
    $isElevated = $prp.IsInRole($adm)
    return $isElevated
}

function Check-NetInterface()
{
    $outputName = "Network Interface"
    $result = [TestResult]::Unset
    $description = " "

    try
    {
        $query = "SELECT * FROM Win32_NetworkAdapter WHERE NOT PNPDeviceID LIKE 'ROOT\\%'"
        $interfaces = Get-WmiObject -Query $query | Sort index
        $networkInterface = @()

        #Save in a string all the interfaces found
        foreach ($interface in $interfaces)
        {
            $name = $interface.NetConnectionID
            $description = $interface.Name

            if ($name)
            {
               $networkInterface += "($name) $description "
            }
        }

        if ($networkInterface)
        {
            $result = [TestResult]::Pass
            $description = $networkInterface -join " - "
        }
        else
        {
            $result = [TestResult]::Fail
            $description = "No network"
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

function Check-CacheFolder()
{
    $outputName = "Cache Folder Access"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        $dosvcWorkingDir = $doConfig.WorkingDirectory
        if (!(Test-Path $dosvcWorkingDir)) { throw "Cache folder not found: $dosvcWorkingDir" }

        $acl = Get-Acl $dosvcWorkingDir

        $IdentityReferenceDO = "NT SERVICE\DoSvc"
        $IdentityReferenceNS = "NT AUTHORITY\NETWORK SERVICE"
        $inheritanceFlags = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor ([System.Security.AccessControl.InheritanceFlags]::ObjectInherit)

        # Filter to DO/NS permissions
        $permissionEntries = $acl.Access | where { @($IdentityReferenceDO, $IdentityReferenceNS) -contains $_.IdentityReference.Value }
        # This might be interesting here: Write-Verbose $permissionEntries

        # Look for Allow/FullControl/Full inheritance
        $permissionEntries = $permissionEntries | where { ($_.AccessControlType -eq "Allow") -and ($_.FileSystemRights -eq "FullControl") -and ($_.InheritanceFlags -eq $inheritanceFlags) }

        if ($permissionEntries)
        {
            $result = [TestResult]::Pass
        }
        else
        {
            $description = "Required permissions missing"
            $result = [TestResult]::Fail
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

function Check-Service([string] $ServiceName)
{
    $outputName = "Service Status"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        $service = Get-Service -Name $ServiceName
        if ($service -and ($service.StartType -ne "Disabled"))
        {
            if ($service.Status -eq "Running")
            {
                $description = "$ServiceName running"
                $result = [TestResult]::Pass
            }
            else
            {
                $description = "$ServiceName stopped"
                $result = [TestResult]::Warn
            }
        }
        else
        {
            $description = "$ServiceName disabled"
            $result = [TestResult]::Fail
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

function Check-KeyAccess()
{
    $outputName = "Registry Key Access"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        Remove-PSDrive HKU -ErrorAction SilentlyContinue

        $drive = New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS
        $testPath = Test-Path -Path HKU:\S-1-5-20\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization
        if (!$testPath) { throw "Registry Key not found" }
        # TODO: Check permissions on key

        # $doConfig.WorkingDirectory is the cache path, which may be redirected elsewhere. The state directory doesn't follow that redirection.
        $path = "$env:windir\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization\State\dosvcState.dat"

        $testPath = Test-Path -Path $path -PathType Leaf
        if (!$testPath)
        {
            $description = "Registry file not found"
            $result = [TestResult]::Fail
        }
        else
        {
            # TODO: Check permissions on file
            $result = [TestResult]::Pass
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
    finally
    {
        Remove-PSDrive HKU -ErrorAction SilentlyContinue
    }
}

function Check-RAMRequired()
{
    $outputName = "RAM"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        $totalRAM = (Get-WmiObject Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object -ExpandProperty Sum)/1GB

        if ($totalRAM -ge $doConfig.MinTotalRAM)
        {
            $description = "$totalRAM GB"
            $result = [TestResult]::Pass
        }
        else
        {
            $description = "Local RAM: $totalRAM GB | RAM Requirements: $($doConfig.MinTotalRAM) GB."
            $result = [TestResult]::Fail
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

function Check-DiskRequired()
{
    $outputName = "Disk"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        $diskSize = Get-WmiObject -Class win32_logicaldisk | Where-Object DeviceId -eq $env:SystemDrive | Select-Object @{N='Disk'; E={$_.DeviceId}}, @{N='Size'; E={[math]::Round($_.Size/1GB,2)}}, @{N='FreeSpace'; E={[math]::Round($_.FreeSpace/1GB,2)}}

        if ($diskSize.FreeSpace -ge $doConfig.MinTotalDiskSize)
        {
            $result = [TestResult]::Pass
            $description = "$($diskSize.Disk) | Total Size: $($diskSize.Size)GB | Free Space: $($diskSize.FreeSpace)GB"
        }
        else
        {
            $result = [TestResult]::Fail
            $description = "Free Space Requirements: $($doConfig.MinTotalDiskSize)GB. | Local Free Space: $($diskSize.FreeSpace)GB"
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

function Check-Vpn()
{
    $outputName = "VPN"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        $vpn = Get-VpnConnection
        if (!$vpn)
        {
            $result = [TestResult]::Pass
        }
        else
        {
            $activeVPN = $vpn | Where-Object ConnectionStatus -eq "Connected"  | Select-Object -ExpandProperty Name
            if ($activeVPN)
            {
                $result = [TestResult]::Warn
                $description = "Connected: $activeVPN"
            }
            else
            {
                $AllVPN = (($vpn | Select-Object -ExpandProperty Name) -join " - ")
                $result = [TestResult]::Pass
                $description = "Not connected: $AllVPN"
            }
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

function Check-PowerBattery()
{
    $outputName = "Power"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        $battery = Get-WmiObject -Class win32_battery

        #PC:
        if (!$battery)
        {
            $result = [TestResult]::Pass
            $plan = Get-WmiObject -Class win32_powerplan -Namespace "root\cimv2\power" | Where-Object IsActive -eq true | Select-Object -ExpandProperty ElementName
            $description = "A/C: $plan"
        }
        #Notebook:
        else
        {
            $batteryPercentage = $battery.EstimatedChargeRemaining
            $batteryStatus = Get-WmiObject -Class BatteryStatus -Namespace root\wmi -ComputerName "localhost" -ErrorAction SilentlyContinue -ErrorVariable ProcessError

            if ($ProcessError)
            {
                $result = [TestResult]::Fail
                $description = "WMI Error ( Check https://learn.microsoft.com/en-us/previous-versions/tn-archive/ff406382(v=msdn.10) ) | Error: $($ProcessError.Exception)"
            }
            elseif ($batteryStatus.PowerOnline)
            {
                $result = [TestResult]::Pass
                $description = "A/C: $batteryPercentage% (charging)"
            }
            else
            {
                $batteryLevelForSeeding = $doConfig.BatteryPctToSeed
                if ($batteryPercentage -ge $batteryLevelForSeeding)
                {
                    $result = [TestResult]::Pass
                }
                else
                {
                    $result = [TestResult]::Fail
                }
                $description = "Battery: $batteryPercentage% ($batteryLevelForSeeding% required to upload)"
            }
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

#----------------------------------------------------------------------------------#
# Connection Check

function Test-Port([int] $Port, [switch] $Optional)
{
    $outputName = "Check Port"
    $oldPreference = $Global:ProgressPreference
    $result = [TestResult]::Unset
    $description = "$Port"

    try
    {
        $Global:ProgressPreference = 'SilentlyContinue'
        $resultTest = Test-NetConnection -Computer localhost -Port $Port -WarningAction SilentlyContinue -InformationLevel 'Quiet'

        if ($resultTest)
        {
            $result = [TestResult]::Pass
        }
        else
        {
            if ($Optional -or ((Check-DownloadMode).Result -ne [TestResult]::Pass))
            {
                $result = [TestResult]::Warn
            }
            else
            {
                $result = [TestResult]::Fail
            }
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
    finally
    {
        $Global:ProgressPreference = $oldPreference
    }
}

function Check-DownloadMode()
{
    $outputName = "Download Mode"
    $result = [TestResult]::Fail
    $downloadMode = $doConfig.DownloadMode

    if (@("Lan", "Group", "Internet") -contains $downloadMode)
    {
        $result = [TestResult]::Pass
    }

    [pscustomobject] @{ Name = $outputName; Result = $result; Details = $downloadMode }
}

function Test-Hostname([string] $HostName)
{
    $outputName = "Host Connection"
    $description = $HostName
    $result = [TestResult]::Unset

    try
    {
        $dnsHostnames = Resolve-DnsName $HostName | Select-Object -Unique -Property NameHost | % {[string]$_.NameHost}
        $dnsHostnames = $dnsHostnames | Where {!$_.Equals("")}

        $result = [TestResult]::Fail

        # Check if the list of hostnames is empty
        if ($dnsHostnames -eq $null)
        {
            $description = "Failed to resolve DNS: $HostName"
        }
        else
        {
            foreach($dnsHostname in $dnsHostnames)
            {
                $test = Test-NetConnection $dnsHostname -Port 80 -WarningAction SilentlyContinue
                if ($test.TcpTestSucceeded)
                {
                    $result = [TestResult]::Pass
                    break
                }
            }
        }

        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }
}

function Get-GeoResponse()
{
    $url = "https://geo.prod.do.dsp.mp.microsoft.com/geo?doClientVersion=$((Get-OSVersion).ToString())"
    $contentType = $null
    $statusCode = 0
    $details = $null
    $success = $false

    try
    {
        $httpResponse = Get-WebRequestData $url

        Write-Verbose $httpResponse.RawContent

        $contentType = $httpResponse.Headers["Content-Type"]
        $statusCode = $httpResponse.StatusCode

        if (($statusCode -eq 200) -and ($contentType -eq "text/json"))
        {
            $details = ConvertFrom-Json $httpResponse.Content
            $success = $true
        }
        else
        {
            $details = $httpResponse.Content
        }
    }
    catch [System.Net.WebException]
    {
        $details = "Unable to reach DO's GEO service. Exception: $($_.Exception.Message)"
    }
    catch
    {
        $details = "HR: $($_.Exception.HResult) - $($_.Exception.Message)"
    }

    [pscustomobject] @{ StatusCode = $statusCode; Type = $contentType; Details = $details; Success = $success}
}

function Test-InternetInfo()
{
    $resultInt = [TestResult]::Fail
    $outputNameInt = "Internet Access"
    $msgInt = ""

    $resultIp = [TestResult]::Fail
    $outputNameIp = "External IP"
    $msgIp = "Unable to get External IP in Geo Response!"

    $testResults = @()
    $geoResponse = Get-GeoResponse

    if (($geoResponse.StatusCode -eq 0) -or ($geoResponse.Type -eq $null) )
    {
        $msgInt = $geoResponse.Details
    }
    elseif ($geoResponse.StatusCode -ne 200)
    {
        $msgInt = "Unable to reach DO's GEO service. Status Code: $($httpResponse.StatusCode) - $($httpResponse.StatusDescription)"
    }
    elseif ($geoResponse.Type -eq "text/html")
    {
        $resultInt = [TestResult]::Warn
        $msgInt = "Possible captive portal detected!"
    }
    elseif ($geoResponse.Type -ne "text/json")
    {
        $msgInt = "Unexpected Content-Type in GEO response: '$contentType'"
    }
    elseif ([string]::IsNullOrEmpty($geoResponse.Details.Version) -or [string]::IsNullOrEmpty($geoResponse.Details.KeyValue_EndpointFullUri))
    {
        $msgInt = "Invalid GEO response: $($geoResponse.Details)"
    }
    elseif ([string]::IsNullOrEmpty($geoResponse.Details.ExternalIpAddress) -or ($geoResponse.Details.ExternalIpAddress -eq "0.0.0.0"))
    {
        $msgInt = "Invalid GEO response!"
        $msgIp = " Invalid External IP in Geo Response! IP: $($geoResponse.Details.ExternalIpAddress)"
    }
    else
    {
        $resultInt = [TestResult]::Pass
        $resultIp  = [TestResult]::Pass

        $msgIp = $geoResponse.Details.ExternalIpAddress
    }

    $testResults += [pscustomobject] @{ Name = $outputNameInt; Result = $resultInt; Details = $msgInt; Connection = ($resultInt -eq [TestResult]::Pass) }
    $testResults += [pscustomobject] @{ Name = $outputNameIp;  Result = $resultIp;  Details = $msgIp }

    return $testResults
}

function Check-ByteRange()
{
    $outputName = "HTTP Byte-Range Support"
    $result = [TestResult]::Unset
    $description = ""

    try
    {
        $uri = "http://dl.delivery.mp.microsoft.com/filestreamingservice/files/52fa8751-747d-479d-8f22-e32730cc0eb1"
        $request = [System.Net.WebRequest]::Create($uri)

        # Set request
        $request.Method = "GET"
        $request.AddRange("bytes", 0, 9)

        $return = $request.GetResponse()
        $statusCode = [int]$return.StatusCode
        $contentRange = $return.GetResponseHeader("Content-Range")
        $description = "$statusCode - $($return.StatusCode) , Content-Range: $contentRange"

        if(($statusCode -eq 206) -and ($contentRange -eq "bytes 0-9/25006511"))
        {
            $result = [TestResult]::Pass
        }
        else
        {
            $result = [TestResult]::Fail
        }

        Write-Verbose $return.Headers.ToString()
        [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
    }
    catch
    {
        [pscustomobject] @{ Name = $outputName; Result = $null; Details = $_.Exception }
    }

}

#----------------------------------------------------------------------------------#
# P2P Check
function Import-Winrt()
{
    $Module = "BurntToast"
    Write-Progress -Activity "Importing WinRT" -Status "Checking Powershell Version" -PercentComplete 0

    # Adding this Start-Sleep to Write-Progress works in Powershell 7
    if ($PSVersionTable.PSVersion.Major -gt 6)
    {
        Start-Sleep -Seconds 1
    }

    Write-Progress -Activity "Importing WinRT" -Status "Load WinRT" -PercentComplete 50
    try
    {
        if ($PSVersionTable.PSVersion.Major -lt 6)
        {
            $null = [Windows.Management.Policies.NamedPolicy,Windows.Management.Policies,ContentType=WindowsRuntime]
        }
        else
        {
            $burntToastIsInstalled = Check-ModuleIsInstalled $Module
            if (-not $burntToastIsInstalled)
            {
                throw "Unable to find $Module installation!"
            }

            $path = (Get-Item (Get-Module -ListAvailable $Module).Path).DirectoryName
            $path = $path + "\lib\Microsoft.Windows.SDK.NET\"
            if (-not (Test-Path -Path $path))
            {
                throw "BurntToast path doesn't exists: $path"
            }

            $dllsPath = Get-ChildItem -Path $path -Filter *.dll -Recurse | %{$_.FullName}
            if (-not $dllsPath)
            {
                throw [System.IO.FileNotFoundException] "Dlls not found in $path"
            }

            Add-Type -AssemblyName $dllsPath
        }
    }
    catch
    {
        Write-Error $_.Exception
    }

    Write-Progress -Activity "Importing WinRT" -Status "Finish WinRT Check" -Completed
}

function Load-Module ([string] $Module)
{
    try
    {
        $oldProgressPreference = $Global:ProgressPreference
        $Global:ProgressPreference = "SilentlyContinue"

        $null = Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
        Install-Module -Name $Module -WarningAction SilentlyContinue -Scope CurrentUser
    }
    catch
    {
        Write-Error $_.Exception
    }
    finally
    {
        if ($oldProgressPreference)
        {
            $Global:ProgressPreference = $oldProgressPreference
        }
    }
}

function Get-PolicyData([string] $PolicyCode)
{
    $return = [string]::Empty
    $description = [string]::Empty
    $fail = $false

    try
    {
        $policy = [Windows.Management.Policies.NamedPolicy]::GetPolicyFromPath("DeliveryOptimization", $PolicyCode)

        if ($policy.IsManaged)
        {
            $description = "Policy set."
            switch ($policy.Kind.ToString())
            {
                "Int32" { $return = $policy.GetInt32().ToString() }
                "Int64" { $return = $policy.GetInt64().ToString() }
                default { $return = $policy.GetString() }
            }
        }
        else
        {
            $description = "Policy not set."
        }
    }
    catch
    {
        $description = "Failure to get policy: $($_.Exception.Message)."
        $fail = $true
    }

    Write-Verbose "The $PolicyCode value is: [$return] - $description"
    [pscustomobject] @{ PolicyCodeValue = $return; Details = $description ; Fail = $fail}
}

function Check-PeerEfficiency()
{
    Write-Progress -Activity "Checking Peer Efficiency" -Status "Gathering data to determine P2P efficiency (it can take a few minutes)" -PercentComplete 10
    $logInfo = "Peer Efficiency for this month: "

    try
    {
        $downloadInfo = Get-DeliveryOptimizationPerfSnapThisMonth
        Write-Verbose $downloadInfo

        $totalPeer = $downloadInfo.DownloadLanBytes + $downloadInfo.DownloadInternetBytes
        if ($totalPeer -eq 0)
        {
            $peerEfficiency = 0
        }
        else
        {
            $totalDownloaded = $downloadInfo.DownloadHttpBytes + $totalPeer
            $peerEfficiency = [math]::Round(($totalPeer * 100 / $totalDownloaded), 2)
        }

        Write-Progress -Activity "Checking Peer Efficiency" -Status "Creating return" -PercentComplete 80
        $description = "$peerEfficiency %"
    }
    catch
    {
        Write-Error $_.Exception
        $description = "Failure in Get-DeliveryOptimizationPerfSnapThisMonth."
    }
    finally
    {
        Write-Progress -Activity "Checking Peer Efficiency" -Status "Returning data" -Completed
    }


    [pscustomobject] @{ Peer_Info = $logInfo; Description = $description}
}

function Get-PeerLogErrors()
{
    # Adding this Start-Sleep to Write-Progress works in Powershell 7
    if ($PSVersionTable.PSVersion.Major -gt 6)
    {
        Start-Sleep -Seconds 1
    }

    Write-Progress -Activity "Finding errors in DO logs" -Status "Parsing logs (it can take a couple of minutes)" -PercentComplete 10

    $startDate = (Get-Date).AddDays(-15)
    $hrRegistered = (Get-DeliveryOptimizationLog -LevelFilter 3) | Where-Object {($_.TimeCreated -gt $startDate) -and ($_.ErrorCode -ne $null)} | Sort-Object -Property ErrorCode -Unique
    Write-Progress -Activity "Finding errors in DO logs" -Status "Filtering errors" -PercentComplete 40

    if($hrRegistered)
    {
        Write-Progress -Activity "Finding errors in DO logs" -Status "Returning errors" -PercentComplete 80
        Get-DOErrorsTable | Where-Object {$hrRegistered.ErrorCode -contains $_.ErrorCode}
    }

    Write-Progress -Activity "Finding errors in DO logs" -Status "Returning errors" -Completed
}

function Get-DOPolicies([pscustomobject] $ErrorsFound)
{
    $policyTable = Get-DOPolicyTable
    $policyOutputs = @()
    $percentComp = 0

    # Adding this Start-Sleep to Write-Progress works in Powershell 7
    if ($PSVersionTable.PSVersion.Major -gt 6)
    {
        Start-Sleep -Seconds 1
    }

    Write-Progress -Activity "Checking DO Policies" -Status "Getting policy data" -PercentComplete $percentComp

    try
    {
        foreach($policy in $policyTable)
        {
            $policyRelatedError = $false
            $policyValue = $null

            Write-Progress -Activity "Checking DO Policies" -Status "Getting $($policy.PolicyCode) data" -PercentComplete $percentComp
            $percentComp += 100/$policyTable.Count

            #Policy Setup adjustments.
            $policyValue = (Get-PolicyData -PolicyCode $policy.PolicyCode).PolicyCodeValue

            if($policyValue)
            {
                if($policy.PolicyUnit)
                {
                    $policyValue += " $($policy.PolicyUnit)"
                }

                if($policy.PolicyCode -eq "DODownloadMode")
                {
                    $policyValue = switch ( $policyValue)
                    {
                        0 { "CdnOnly - 0" }
                        1 { "LAN - 1" }
                        2 { "Group - 2" }
                        3 { "Internet - 3" }
                        99 { "Simple - 99" }
                        100 { "Bypass - 100" }
                        default {  $policyValue }
                    }
                }
            }

            $policyError = $ErrorsFound | Where-Object {$_.RelatedPolicyName -eq $policy.PolicyCode}

            if ($policyError)
            {
                $description = $policyError.SuggestedRemedy
                $policyRelatedError = $true
            }
            else
            {
                $description = $policy.Description
            }

            $description += "`r`nMORE INFO: $($policy.Link)`r`n"
            $policyOutputs += [pscustomobject] @{ Name = $policy.PolicyName; Configuration = $policyValue; MoreInfo = $description; PolicySuggestion = $policyRelatedError }
        }
    }
    catch
    {
        Write-Error $_.Exception
    }

    Write-Progress -Activity "Checking DO Policies" -Status "Returning data" -Completed
    return $policyOutputs
}

#----------------------------------------------------------------------------------#
# MCC Check
function Check-ConfiguredCacheHostServer()
{
    $outputName = "DOCacheHost Server Configured"
    $progressActivity = "Checking if device is configured to use CacheHost Server"
    $description = [string]::Empty

    Write-Progress -Activity $progressActivity -Status "Checking Windows 11 Settings" -PercentComplete 10
    $vpnCacheServerPolicyInWin11 = Get-DODisallowCacheServerPolicyInWin11
    if($vpnCacheServerPolicyInWin11.disallowMccOnVpn)
    {
        if ($vpnCacheServerPolicyInWin11.VpnConnected)
        {
            return [pscustomobject] @{ Name = $outputName; Result = [TestResult]::Fail; Details = "MCC usage is disabled because DO is set to disallow CacheServer downloads on VPN, and you are connected to a VPN." }
        }
        else
        {
            $description += "[Warning: DODisallowCacheServerDownloadsOnVPN is set, so a VPN connection will disallow CacheServer downloads] "
        }
    }

    Write-Progress -Activity $progressActivity -Status "Getting Cache Host Data" -PercentComplete 40
    $mccHostInfo = Get-CacheHostServer

    Write-Progress -Activity $progressActivity -Status "Validating Data" -PercentComplete 70
    # ------------------------------------------------------------------ #
    # 1 -> Validate if no MCC config was found
    # [ERROR]
    $descriptionValidation = Check-CacheHostNotFound -CacheHostObject $mccHostInfo
    if ($descriptionValidation)
    {
        $description += $descriptionValidation
        return [pscustomobject] @{ Name = $outputName; Result = [TestResult]::Fail; Details = $description }
    }

    # ------------------------------------------------------------------ #
    # 2 -> Validate if PolicyCacheHostSource is DHCPOption235Force and, CacheHostPolicy and DHCPOptionSet are set
    # [WARN] - Returning DHCP Option Value
    $descriptionValidation = Check-DHCPOption235WithCacheHostPolicyAndDHCPOptionSet -CacheHostObject $mccHostInfo
    if ($descriptionValidation.Description)
    {
        $description += $descriptionValidation.Description
        return [pscustomobject] @{ Name = $outputName; Result = [TestResult]::Warn; Details = $descriptionValidation.Description; MCCHost = $descriptionValidation.CacheHost }
    }

    # ------------------------------------------------------------------ #
    # 3 -> Validate if GeoCacheHostFlag is set and possibles cache hosts.
    # [SUCCESS]
    $geoCacheHostFlagValidation = Check-GeoCacheHostFlagSetToUseDhcpOption -CacheHostObject $mccHostInfo
    if ($geoCacheHostFlagValidation.Description)
    {
        $description += $geoCacheHostFlagValidation.Description
        return [pscustomobject] @{ Name = $outputName; Result = [TestResult]::Warn; Details = $description; MCCHost = $geoCacheHostFlagValidation.CacheHost }
    }

    # ------------------------------------------------------------------ #
    # 4 -> Validate DisableDNSSD cases.
    # [SUCCESS]
    if ($mccHostInfo.PolicyCacheHostSource -eq [CacheHostSource]::DisableDNSSD)
    {
        $disableDNSSDValidation = Check-DisableDNSSDCases -CacheHostObject $mccHostInfo
        $description += $disableDNSSDValidation.Description
        return [pscustomobject] @{ Name = $outputName; Result = [TestResult]::Pass; Details = $description; MCCHost = $disableDNSSDValidation.CacheHost }
    }

    # ------------------------------------------------------------------ #
    # 5 -> Validate DHCP Option235 cases.
    # [SUCCESS]
    $dhcpOption235Validation = Check-DHCPOption235Cases -CacheHostObject $mccHostInfo
    $description += $dhcpOption235Validation.Description
    return [pscustomobject] @{ Name = $outputName; Result = [TestResult]::Pass; Details = $description; MCCHost = $dhcpOption235Validation.CacheHost }

    Write-Progress -Activity $progressActivity -Status "Returning data" -Completed
}

function Check-DHCPOption235Cases([pscustomobject] $CacheHostObject)
{

    $description = "This device has CacheHostSource configured to use <$([Int32]$CacheHostObject.PolicyCacheHostSource) = $($CacheHostObject.PolicyCacheHostSource)>, so it will dynamically use "
    $cacheHost = [string]::Empty

    if (!$CacheHostObject.DHCPCacheHost)
    {
        if($CacheHostObject.GeoCacheHost)
        {
            $description += "the Cache Host '$($CacheHostObject.GeoCacheHost)' set via Delivery Optimization Cloud Service."
            $cacheHost = $CacheHostObject.GeoCacheHost
        }
        else
        {
            $description += "the Cache Host '$($CacheHostObject.PolicyCacheHost)' set via 'DOCacheHost' Policy"
            $cacheHost = $CacheHostObject.PolicyCacheHost
        }
    }
    else
    {
        $description += "the DHCP option: $($CacheHostObject.DHCPCacheHost)."
        $cacheHost = $CacheHostObject.DHCPCacheHost
    }

    return [pscustomobject] @{ Description = $description; CacheHost = $cacheHost }
}

function Check-DisableDNSSDCases([pscustomobject] $CacheHostObject)
{
    $description = "This device has CacheHostSource configured to use <0 = DisableDNSSD> and "
    $cacheHost = [string]::Empty

    if($CacheHostObject.GeoCacheHost)
    {
        $description += "The CacheHost server '$($CacheHostObject.GeoCacheHost)' is set via Delivery Optimization Cloud Service."
        $cacheHost = $CacheHostObject.GeoCacheHost
    }
    else
    {
        $description += "The CacheHost server '$($CacheHostObject.PolicyCacheHost)' is set via DOCacheHost Policy."
        $cacheHost = $CacheHostObject.PolicyCacheHost
    }

    return [pscustomobject] @{ Description = $description; CacheHost = $cacheHost }
}

function Check-GeoCacheHostFlagSetToUseDhcpOption([pscustomobject] $CacheHostObject)
{
    $description = [string]::Empty
    $cacheHost = [string]::Empty

    if ($CacheHostObject.GeoDhcpFlagIsSet)
    {
        if (($CacheHostObject.DHCPCacheHost) -and ($CacheHostObject.GeoCacheHost))
        {
            $description = "Both DHCP option $($CacheHostObject.DHCPCacheHost) and GeoCacheHost $($CacheHostObject.GeoCacheHost) are set, but $($CacheHostObject.GeoCacheHost) will be used as MCC Cache Server because DHCPOption235 flag is set in Delivery Optimization Cloud Service."
            $cacheHost = $($CacheHostObject.GeoCacheHost)
        }
        elseif ($CacheHostObject.DHCPCacheHost)
        {
            $description = "The DHCP option $($CacheHostObject.DHCPCacheHost) will be dynamically used as MCC Cache Server because DHCPOption235 flag is set in Delivery Optimization Cloud Service."
            $cacheHost = $CacheHostObject.DHCPCacheHost
        }
        else
        {
            $description = "The GeoCacheHost $($CacheHostObject.GeoCacheHost) will be dynamically used as MCC Cache Server because DHCPOption235 flag is set in Delivery Optimization Cloud Service."
            $cacheHost = $CacheHostObject.GeoCacheHost
        }
    }

    return [pscustomobject] @{ Description = $description; CacheHost = $cacheHost }
}

function Check-DHCPOption235WithCacheHostPolicyAndDHCPOptionSet([pscustomobject] $CacheHostObject)
{
    $description = [string]::Empty
    $cacheHost = $null

    if (($CacheHostObject.PolicyCacheHostSource -eq [CacheHostSource]::DHCPOption235Force) -and
            ![string]::IsNullOrEmpty($CacheHostObject.PolicyCacheHost) -and
            ![string]::IsNullOrEmpty($CacheHostObject.DHCPCacheHost))
    {
        $description = "Detecting both CacheHost Policy and DHCP Option set. Therefore, DHCP Option 235 Force string $($CacheHostObject.DHCPCacheHost) will be used according < $([Int32]$CacheHostObject.PolicyCacheHostSource) = $($CacheHostObject.PolicyCacheHostSource)>."
        $cacheHost = $CacheHostObject.DHCPCacheHost
    }
    elseif (($CacheHostObject.PolicyCacheHostSource -eq [CacheHostSource]::DHCPOption235) -and
            ![string]::IsNullOrEmpty($CacheHostObject.PolicyCacheHost) -and
            ![string]::IsNullOrEmpty($CacheHostObject.DHCPCacheHost))
    {
        $description = "Detecting both CacheHost Policy and DHCP Option set. Therefore, the Cache Host Policy string $($CacheHostObject.PolicyCacheHost) will be used according < $([Int32]$CacheHostObject.PolicyCacheHostSource) = $($CacheHostObject.PolicyCacheHostSource)>."
        $cacheHost = $CacheHostObject.PolicyCacheHost
    }

    return [pscustomobject] @{ Description = $description; CacheHost = $cacheHost }
}

function Check-CacheHostNotFound([pscustomobject] $CacheHostObject)
{
    $infoPage = "https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-reference#cache-server-hostname"
    $description = [string]::Empty

    if ([string]::IsNullOrEmpty($CacheHostObject.PolicyCacheHost) -and
        [string]::IsNullOrEmpty($CacheHostObject.GeoCacheHost)    -and
        [string]::IsNullOrEmpty($CacheHostObject.DHCPCacheHost))
    {
        $description = "Unable to get any MCC CacheHost configuration on this device. For more information: $infoPage"
    }

    return $description
}

function Get-CacheHostServer()
{
    $dhcpGeoFlag = $false
    $cacheHostPolicy = $null
    $dhcpCacheHost = $null
    $cacheHostSourcePolicy = Get-DOCacheHostSource

    if($cacheHostSourcePolicy -in [CacheHostSource]::DHCPOption235, [CacheHostSource]::DHCPOption235Force)
    {
        $dhcpCacheHost = Get-DhcpStringOptionValue([Int32][DhcpOption]::DOMccHost)
    }

    if (([string]::IsNullOrEmpty($dhcpCacheHost)) -or ($cacheHostSourcePolicy -ne [CacheHostSource]::DHCPOption235Force))
    {
        $cacheHostPolicy = (Get-PolicyData -PolicyCode "DOCacheHost").PolicyCodeValue
    }

    if(([string]::IsNullOrEmpty($cacheHostPolicy)) -and ([string]::IsNullOrEmpty($dhcpCacheHost)))
    {
        $geoCacheHostRequest = Get-CacheHostServerFromGeoService
        $geoCacheHost = $geoCacheHostRequest.CacheHost
        if($geoCacheHostRequest.CacheHostFlag -eq 1)
        {
            $dhcpGeoFlag = $true
            $dhcpCacheHost = Get-DhcpStringOptionValue([Int32][DhcpOption]::DOMccHost)
        }
    }

    $returnValue = [pscustomobject] @{ PolicyCacheHost = $cacheHostPolicy; PolicyCacheHostSource = $cacheHostSourcePolicy; GeoCacheHost = $geoCacheHost; GeoDhcpFlagIsSet = $dhcpGeoFlag; DHCPCacheHost = $dhcpCacheHost}
    Write-Verbose $returnValue
    $returnValue
}

function Get-DOCacheHostSource()
{
    $cacheHostSourcePolicy = (Get-PolicyData -PolicyCode "DOCacheHostSource").PolicyCodeValue

    if (![string]::IsNullOrEmpty($cacheHostSourcePolicy) -and ([Int32]$cacheHostSourcePolicy -le [CacheHostSource]::DHCPOption235Force))
    {
        [System.Enum]::Parse([CacheHostSource], $cacheHostSourcePolicy)
    }
}

function Get-CacheHostServerFromGeoService()
{
    $geoResponse = Get-GeoResponse
    Write-Verbose $geoResponse.Details

    [pscustomobject] @{ CacheHost = $geoResponse.Details.CacheHost; CacheHostFlag = $geoResponse.Details.CacheHostFlag }
}

function Get-DODisallowCacheServerPolicyInWin11()
{
    $isWin11 = $false
    $disallowMccOnVpn = $false
    $vpnConn = $false

    try
    {
        $isWin11 = ((Get-OSVersion).Build -ge 22621)
        if ($isWin11)
        {
            $disallowMccOnVpn = ((Get-PolicyData -PolicyCode "DODisallowCacheServerDownloadsOnVPN").PolicyCodeValue) -eq "1"

            if($disallowMccOnVpn -eq $true)
            {
                $vpn = Get-VPNconnection | Where-Object {$_.ConnectionStatus -eq "Connected"}

                if ($vpn.Length -ne 0) { $vpnConn = $true }
            }
        }
    }
    catch
    {
        Write-Error $_.Exception
    }

    [pscustomobject] @{ DisallowMccOnVpn = $disallowMccOnVpn; VpnConnected = $vpnConn }
}

function Add-PInvokeTypes()
{
    $csharpCode = @'
    namespace Microsoft.DO.PInvoke
    {
        using System;
        using System.Net;
        using System.Net.NetworkInformation;
        using System.Net.Sockets;
        using System.Runtime.InteropServices;
        using System.Text;
 
        public static class Dhcp
        {
            private const uint DhcpApiRequest_Synchronous = 0x02;
 
            [StructLayout(LayoutKind.Sequential)]
            private struct DHCPAPI_PARAMS
            {
                public uint Flags;
                public uint OptionId;
                [MarshalAs(UnmanagedType.Bool)]
                public bool IsVendor;
                public IntPtr Data;
                public uint nBytesData;
            }
 
            [StructLayout(LayoutKind.Sequential)]
            private struct DHCPCAPI_PARAMS_ARRAY
            {
                public uint nParams;
                public IntPtr Params;
            }
 
            [DllImport("dhcpcsvc.dll", SetLastError = false)]
            private static extern uint DhcpCApiInitialize(out uint Version);
 
            [DllImport("dhcpcsvc.dll", SetLastError = false)]
            private static extern void DhcpCApiCleanup();
 
            [DllImport("dhcpcsvc.dll", SetLastError = false)]
            private static extern uint DhcpRequestParams([In, Optional] uint Flags, [In, Optional] IntPtr Reserved,
                [MarshalAs(UnmanagedType.LPWStr)] string AdapterName, [In, Optional] IntPtr ClassId, [In] DHCPCAPI_PARAMS_ARRAY SendParams,
                [In, Out] DHCPCAPI_PARAMS_ARRAY RecdParams, [Out] IntPtr Buffer, ref uint BufferSizeHolder,
                [Optional, MarshalAs(UnmanagedType.LPWStr)] string RequestIdStr);
 
            private static byte[] GetDhcpOption(string adapterGuid, uint optionId)
            {
                uint version = 0;
                uint result = DhcpCApiInitialize(out version);
                if (result != 0)
                {
                    throw new Exception(string.Format("DhcpCApiInitialize failed with error code {0}", result));
                }
 
                IntPtr reqParamsBuffer = IntPtr.Zero;
                try
                {
                    var sendParams = new DHCPCAPI_PARAMS_ARRAY()
                    {
                        nParams = 0,
                        Params = IntPtr.Zero,
                    };
 
                    DHCPAPI_PARAMS optionParam = new DHCPAPI_PARAMS()
                    {
                        Flags = 0,
                        OptionId = optionId,
                        IsVendor = false,
                        Data = IntPtr.Zero,
                        nBytesData = 0
                    };
 
                    var requestParams = new DHCPCAPI_PARAMS_ARRAY()
                    {
                        nParams = 1,
                        Params = Marshal.AllocHGlobal(Marshal.SizeOf<DHCPAPI_PARAMS>())
                    };
                    Marshal.StructureToPtr(optionParam, requestParams.Params, false);
 
                    // 1024 bytes should be enough for our purposes
                    reqParamsBuffer = Marshal.AllocHGlobal(1024);
                    uint bufferSize = 1024;
                    result = DhcpRequestParams(
                        Flags: DhcpApiRequest_Synchronous,
                        Reserved: IntPtr.Zero,
                        AdapterName: adapterGuid,
                        ClassId: IntPtr.Zero,
                        SendParams: sendParams,
                        RecdParams: requestParams,
                        Buffer: reqParamsBuffer,
                        BufferSizeHolder: ref bufferSize,
                        RequestIdStr: null);
                    if (result != 0)
                    {
                        throw new Exception(string.Format("DhcpRequestParams failed with error code {0}", result));
                    }
 
                    var recdParam = Marshal.PtrToStructure<DHCPAPI_PARAMS>(requestParams.Params);
                    if ((recdParam.Data == IntPtr.Zero) || (recdParam.nBytesData == 0))
                    {
                        return null;
                    }
 
                    byte[] optionValueData = new byte[recdParam.nBytesData];
                    Marshal.Copy(recdParam.Data, optionValueData, 0, (int)recdParam.nBytesData);
                    return optionValueData;
                }
                finally
                {
                    if (reqParamsBuffer != IntPtr.Zero)
                    {
                        Marshal.FreeHGlobal(reqParamsBuffer);
                    }
 
                    DhcpCApiCleanup();
                }
            }
 
            public static string GetDhcpOptionString(string adapterGuid, uint optionId)
            {
                byte[] optionValueData = GetDhcpOption(adapterGuid, optionId);
                if (optionValueData == null || optionValueData.Length == 0)
                {
                    return string.Empty;
                }
                string dhcpOption = Encoding.ASCII.GetString(optionValueData);
                return dhcpOption.EndsWith("\0") ? dhcpOption.Remove(dhcpOption.Length - 1) : dhcpOption;
            }
 
            public static uint? GetDhcpOptionUInt32(string adapterGuid, uint optionId)
            {
                byte[] optionValueData = GetDhcpOption(adapterGuid, optionId);
                if (optionValueData == null || optionValueData.Length == 0)
                {
                    return null;
                }
                return BitConverter.ToUInt32(optionValueData, 0);
            }
        }
 
        public static class Net
        {
            private struct NET_LUID
            {
                public ulong Value;
            }
 
            [DllImport("iphlpapi.dll", SetLastError = true)]
            private static extern uint ConvertInterfaceGuidToLuid(ref Guid Guid, out NET_LUID Luid);
 
            private static NET_LUID GetLuidForNetworkAdapter(NetworkInterface adapter)
            {
                NET_LUID luid = new NET_LUID();
                var guid = Guid.Parse(adapter.Id);
                uint result = ConvertInterfaceGuidToLuid(ref guid, out luid);
                if (result != 0)
                {
                    throw new System.ComponentModel.Win32Exception();
                }
                return luid;
            }
 
            private const int IF_MAX_STRING_SIZE = 256;
            private const int IF_MAX_PHYS_ADDRESS_LENGTH = 32;
 
            [StructLayout(LayoutKind.Sequential)]
            private struct MIB_IF_ROW2
            {
                public NET_LUID InterfaceLuid;
                public uint InterfaceIndex;
                public Guid InterfaceGuid;
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = (IF_MAX_STRING_SIZE + 1)*2)]
                public byte[] Alias;
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = (IF_MAX_STRING_SIZE + 1)*2)]
                public byte[] Description;
                public uint PhysicalAddressLength;
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = IF_MAX_PHYS_ADDRESS_LENGTH)]
                public byte[] PhysicalAddress;
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = IF_MAX_PHYS_ADDRESS_LENGTH)]
                public byte[] PermanentPhysicalAddress;
                public uint Mtu;
                public uint Type;
                public uint TunnelType;
                public uint MediaType;
                public uint PhysicalMediumType;
                public uint AccessType;
                public uint DirectionType;
                public uint InterfaceAndOperStatusFlags;
                public uint OperStatus;
                public uint AdminStatus;
                public uint MediaConnectState;
                public Guid NetworkGuid;
                public uint ConnectionType;
                public ulong TransmitLinkSpeed;
                public ulong ReceiveLinkSpeed;
                public ulong InOctets;
                public ulong InUcastPkts;
                public ulong InNUcastPkts;
                public ulong InDiscards;
                public ulong InErrors;
                public ulong InUnknownProtos;
                public ulong InUcastOctets;
                public ulong InMulticastOctets;
                public ulong InBroadcastOctets;
                public ulong OutOctets;
                public ulong OutUcastPkts;
                public ulong OutNUcastPkts;
                public ulong OutDiscards;
                public ulong OutErrors;
                public ulong OutUcastOctets;
                public ulong OutMulticastOctets;
                public ulong OutBroadcastOctets;
                public ulong OutQLen;
            }
 
            // Enum to help interpret the bit fields that make up MIB_IF_ROW2.InterfaceAndOperStatusFlags.
            [Flags]
            private enum eInterfaceAndOperStatusFlags
            {
                None = 0,
                HardwareInterface = 1 << 0,
                FilterInterface = 1 << 1,
                ConnectorPresent = 1 << 2,
                NotAuthenticated = 1 << 3,
                NotMediaConnected = 1 << 4,
                Paused = 1 << 5,
                LowPower = 1 << 6,
                EndPointInterface = 1 << 7
            }
 
            [DllImport("iphlpapi.dll", CharSet = CharSet.Auto)]
            private static extern uint GetIfEntry2(ref MIB_IF_ROW2 pIfRow);
 
            public static bool IsConnectorPresent(NetworkInterface adapter)
            {
                var luid = GetLuidForNetworkAdapter(adapter);
                var row = new MIB_IF_ROW2
                {
                    InterfaceLuid = luid
                };
                uint result = GetIfEntry2(ref row);
                if (result != 0)
                {
                    throw new System.ComponentModel.Win32Exception();
                }
 
                var flags = (eInterfaceAndOperStatusFlags)row.InterfaceAndOperStatusFlags;
                return (flags & eInterfaceAndOperStatusFlags.ConnectorPresent) != 0;
            }
        }
    }
'@
;

    # If source code is changed and script is executed in the same PS session, then Add-Type will fail.
    # Close and reopen the PS session to workaround this.
    Add-Type -TypeDefinition $csharpCode
}

function Confirm-IsLinkLocal([IPAddress] $Ip)
{
    $ipBytes = $Ip.GetAddressBytes()
    return (($ipBytes[0] -eq 169) -and ($ipBytes[1] -eq 254))
}

function Confirm-HasPreferredAddress([System.Net.NetworkInformation.NetworkInterface] $Adapter)
{
    $preferredIpv4Address = $Adapter.GetIPProperties().UnicastAddresses.Address |
        Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork }
    return ($preferredIpv4Address -and ![IPAddress]::IsLoopback($preferredIpv4Address) `
        -and !(Confirm-IsLinkLocal -Ip $preferredIpv4Address))
}

function Confirm-PhysicalNetworkType([System.Net.NetworkInformation.NetworkInterface] $Adapter)
{
    # Check IsConnectorPresent first because checking NetworkInterfaceType alone is insufficient.
    # Example: .NET reports Ethernet type for both physical and virtual NICs.
    $isConnectorPresent = [Microsoft.DO.PInvoke.Net]::IsConnectorPresent($Adapter);
    if ($isConnectorPresent)
    {
        $physicalNetworkType = @([System.Net.NetworkInformation.NetworkInterfaceType]::Ethernet,
                            [System.Net.NetworkInformation.NetworkInterfaceType]::FastEthernetT,
                            [System.Net.NetworkInformation.NetworkInterfaceType]::FastEthernetFx,
                            [System.Net.NetworkInformation.NetworkInterfaceType]::GigabitEthernet,
                            [System.Enum]::Parse([System.Net.NetworkInformation.NetworkInterfaceType], 55),
                            [System.Net.NetworkInformation.NetworkInterfaceType]::Wireless80211,
                            [System.Enum]::Parse([System.Net.NetworkInformation.NetworkInterfaceType], 161))
        return $physicalNetworkType.Contains($Adapter.NetworkInterfaceType)
    }
}

# This and other related methods are based on DO client's native code so that we find the same network interface.
# This ensures the Troubleshooter and DO client's results match.
function Get-LocalIPv4Adapter()
{
    $validVirtualInterface = $null

    # Find a physical adapter or, if one is not present, the first virtual adapter, that is in running state
    # and has the preferred IPv4 address (not loopback and not link-local).
    $adapters = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()
    foreach ($adapter in $adapters)
    {
        $statusCheck = $adapter.OperationalStatus -eq [System.Net.NetworkInformation.OperationalStatus]::UP
        $addrCheck = Confirm-HasPreferredAddress $adapter
        $isPhysical = Confirm-PhysicalNetworkType $adapter
        $resultString = "Checks: Status = $statusCheck, Address = $addrCheck, IsPhysical = $isPhysical"
        Write-Verbose "Testing NIC: $($adapter.Id), $($adapter.Name), $($adapter.Description) => $resultString"

        if ($statusCheck -and $addrCheck)
        {
            if ($isPhysical)
            {
                return $adapter
            }

            # We have a virtual interface, use it if it's the first time through here
            # and continue the loop to look for physical ones.
            if (-not $validVirtualInterface)
            {
                $validVirtualInterface = $adapter
            }
        }
    }

    if ($validVirtualInterface)
    {
        return $validVirtualInterface
    }
}

function Get-DhcpStringOptionValue([Int32] $OptionNumber)
{
    Add-PInvokeTypes

    $foundAdapter = Get-LocalIPv4Adapter
    if (-not $foundAdapter)
    {
        Write-Verbose "No suitable network adapter found"
        return $null
    }

    $adapterDesc = "ID = $($foundAdapter.Id), name = $($foundAdapter.Name), description = $($foundAdapter.Description)"
    Write-Verbose "To query DHCP option = $OptionNumber, found IPv4 adapter: $adapterDesc"
    $adapterGuid = $foundAdapter.Id;

    try
    {
        $strOptionValue = [Microsoft.DO.PInvoke.Dhcp]::GetDhcpOptionString($adapterGuid, $OptionNumber)
        if (-not $strOptionValue)
        {
            # As a sanity check, see if subnet mask can be retrieved. It must be present in all IPv4 network adapters.
            # So, if this works, then we have confidence that the net adapter and P/Invoke code is working fine.
            # TODO Remove this once we are able to test the Option235 value in some environment (Azure?).
            $subnetMaskOption = 1
            $subnetMask = [Microsoft.DO.PInvoke.Dhcp]::GetDhcpOptionUInt32($adapterGuid, $subnetMaskOption)
            if ($subnetMask)
            {
                $subnetMaskString = ($subnetMask -as [ipaddress]).IPAddressToString
                Write-Verbose "Subnet mask from DHCP options = $subnetMaskString"
            }
            else
            {
                Write-Verbose "Subnet mask cannot be retrieved from DHCP options"
            }
        }
        return $strOptionValue
    }
    catch
    {
        Write-Error $_.Exception
    }
}

function Check-DownloadCacheHostServer ([string] $CacheHost)
{
    $progressActivity = "Downloading content from CacheHost Server"
    $downloadsInfo = @()
    $result = [TestResult]::Fail
    $description = [string]::Empty

    Write-Progress -Activity $progressActivity -Status "Creating MCC URL to download" -PercentComplete 10
    if ([string]::IsNullOrEmpty($CacheHost))
    {
        return [pscustomobject] @{ Name = "Download from CacheHost server"; Result = $result; Details = "Cache Host Server not found." }
    }

    Write-Progress -Activity $progressActivity -Status "Download using MCC URL" -PercentComplete 50
    $mccServers = $CacheHost -split ","
    foreach($mccServer in $mccServers)
    {
        $mccUrl = Get-MccDownloadTestUrl -CacheHost $mccServer
        try
        {
            $result = [TestResult]::Fail
            $downloadInfo = Get-WebRequestData $mccUrl
            Write-Verbose $downloadInfo.Headers

            if ($downloadInfo.StatusCode -ne 200)
            {
                $description = "Unexpected Status Code: $($downloadInfo.StatusCode) - $($downloadInfo.StatusDescription)"
            }
            elseif ($($downloadInfo.Headers.'Content-Type') -ne "application/octet-stream")
            {
                $description = "Unexpected Content-Type (Check possible captive portal): $($downloadInfo.Headers.'Content-Type')"
            }
            elseif ($downloadInfo.RawContentLength -ne 302341)
            {
                $description = "Incorrect content length size. Expected: 302341 - Received: $($downloadInfo.RawContentLength)"
            }
            else
            {
                $result = [TestResult]::Pass
                $description = "MCC Download: Status = $($downloadInfo.StatusCode), File size: $($downloadInfo.RawContentLength), Address = $mccUrl"
            }
        }
        catch
        {
            $description = "Unable to make a WebRequest using MCC Link! Exception: $($_.Exception)"
        }

        $downloadsInfo += [pscustomobject] @{ Name = "Download from CacheHost server"; Result = $result; Details = $description }
    }

    Write-Progress -Activity $progressActivity -Status "Returning data" -Completed
    return $downloadsInfo
}

function Get-MccDownloadTestUrl([string] $CacheHost)
{
    # Use the file hosted on FSS that serves as the basic test for a working MCC setup in the network
    return "http://$CacheHost/filestreamingservice/files/7bc846e0-af9c-49be-a03d-bb04428c9bb5/Microsoft.png?cacheHostOrigin=dl.delivery.mp.microsoft.com"
}

function Check-DownloadPercentageCacheHost()
{
    $outputName = "Percentage of Download using DOCacheHost"
    $progressActivity = "Calculating Percentage of Download"
    $result = [TestResult]::Fail
    $percentageCacheServer = 0

    Write-Progress -Activity $progressActivity -Status "Get DeliveryOptimization Download Information" -PercentComplete 10
    $downloadInformation = Get-DeliveryOptimizationPerfSnapThisMonth
    if(-not $downloadInformation)
    {
        return [pscustomobject] @{ Name = $outputName; Result = $result; Details = "Unable to get results from Get-DeliveryOptimizationPerfSnapThisMonth." }
    }

    Write-Progress -Activity $progressActivity -Status "Calculate Percentage of CacheServer" -PercentComplete 50
    if ($downloadInformation.DownloadCacheHostBytes -ne 0)
    {
        $percentageCacheServer = 100 * ($downloadInformation.DownloadCacheHostBytes / ($downloadInformation.DownloadHttpBytes + $downloadInformation.DownloadLanBytes + $downloadInformation.DownloadInternetBytes))
        $percentageCacheServer = [math]::Round($percentageCacheServer, 2)
        $description = "This device has downloaded content from MCC this month. To improve the efficiency, check the 'DelayCacheServer' configuration below."
    }
    else
    {
        $description = "This device has not downloaded content from MCC this month."
    }

    $result = "$percentageCacheServer%"

    Write-Progress -Activity $progressActivity -Status "Returning data" -Completed
    return [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
}

function Check-DelayCacheServerFallbackPolicy($PolicyName, $RecommendedValue)
{
    $outputName = "Check $($PolicyName.Substring(2))"
    $result = 0

    $policyInfo = Get-PolicyData -PolicyCode $PolicyName

    if ($policyInfo.Fail)
    {
        $description = $policyInfo.Details
    }
    elseif ($policyInfo.PolicyCodeValue)
    {
        $description = "$($policyInfo.Details) To improve % from Cache server try setting this value to $RecommendedValue seconds as a starting point."
        $result = $policyInfo.PolicyCodeValue
    }
    else
    {
        $description = "$($policyInfo.Details) By increasing this value it may improve chances of foreground downloads to pull from MCC server. Try $RecommendedValue seconds as a starting point."
    }


    return [pscustomobject] @{ Name = $outputName; Result = $result; Details = $description }
}


#----------------------------------------------------------------------------------#
# Aux Functions
function Add-Space([string] $Text, [int] $SizeSpace)
{
    return $Text + (" " * ([math]::max(0, $sizeSpace - $text.Length)))
}

function Get-OSVersion()
{
    return [Environment]::OSVersion.Version
}

function Get-WebRequestData([string] $Url)
{
    # To avoid the error "Internet Explorer engine is not available", it's advisable to create all Web requests use basic parsing only.
    # Beginning with PowerShell 6.0.0, all Web requests use basic parsing only and this option has been deprecated.
    if ($PSVersionTable.PSVersion.Major -gt 5)
    {
        Invoke-WebRequest -Uri $Url
    }
    else
    {
        Invoke-WebRequest -Uri $Url -UseBasicParsing
    }
}

function Check-ModuleIsInstalled([string] $Module)
{
    $ModuleInstalled = $null

    try
    {
        # If module is imported in the session
        $checkModuleSession = Get-Module $Module

        if ($checkModuleSession)
        {
            $ModuleInstalled = $true
            Write-Verbose "$Module was already imported in the session."
        }
        else
        {
            # If module is not imported, but available on disk
            $checkModuleAvailableDisk = Get-Module -ListAvailable | Where-Object {$_.Name -eq $Module}

            if ($checkModuleAvailableDisk)
            {
                Import-Module -ModuleInfo $checkModuleAvailableDisk
                $ModuleInstalled = $true
                Write-Verbose "$Module was installed, but it has to be imported in the session."
            }
            else
            {
                $ModuleInstalled = $false
                Write-Verbose "$Module is not installed."
            }
        }
    }
    catch
    {
        Write-Error $_.Exception
    }

    return $ModuleInstalled
}

#----------------------------------------------------------------------------------#
# MAIN FUNCTIONS:
# Heath Checker:
function Invoke-HealthChecker()
{
    Print-Title " Device Health Check:"
    Write-Host ""
    Print-SubTitle "Device Settings"

    $deviceSettings = @()
    $deviceSettings += Check-DownloadMode
    $deviceSettings += Check-Service -ServiceName "dosvc"
    $deviceSettings += Check-CacheFolder
    $deviceSettings += Check-KeyAccess
    $deviceSettings += Check-Vpn
    Format-ResultObject $deviceSettings

    Print-SubTitle "Hardware Settings"
    $hardwareCheck = @()
    $hardwareCheck += Check-RAMRequired
    $hardwareCheck += Check-DiskRequired
    $hardwareCheck += Check-PowerBattery
    Format-ResultObject $hardwareCheck

    Print-SubTitle "Connection Check"
    Write-Progress -Activity "Connection Check" -Status "Checking net interface" -PercentComplete 0
    $connectionCheck = @()
    $connectionCheck += Check-NetInterface
    Write-Progress -Activity "Connection Check" -Status "Testing port 7680" -PercentComplete 15
    $connectionCheck += Test-Port -Port 7680           # 7680 - DO port
    Write-Progress -Activity "Connection Check" -Status "Testing port 7680" -PercentComplete 30
    $connectionCheck += Test-Port -Port 3544 -Optional # 3544 - Teredo port
    Write-Progress -Activity "Connection Check" -Status "Testing internet connection" -PercentComplete 45
    $connInformation = Test-InternetInfo
    $connectionCheck += $connInformation

    $hostNames = @( "dl.delivery.mp.microsoft.com", "download.windowsupdate.com" )
    if ($connInformation.Connection -eq $true)
    {
        Write-Progress -Activity "Connection Check" -Status "Checking HTTP ByteRange" -PercentComplete 60
        $connectionCheck += Check-ByteRange

        Write-Progress -Activity "Connection Check" -Status "Checking hostnames" -PercentComplete 75
        foreach($hostName in $hostNames)
        {
            $connectionCheck += Test-Hostname -HostName $hostName
        }
    }
    else
    {
        $result = [TestResult]::Fail
        $description = "Internet check failed. Unable to check "

        #Check-ByteRange:
        $connectionCheck += [pscustomobject] @{ Name = "HTTP Byte-Range Support"; Result = $result; Details = ($description + "HTTP Byte-Range Support") }

        #Test-Hostname:
        foreach($hostName in $hostNames)
        {
            $connectionCheck += [pscustomobject] @{ Name = "Host Connection"; Result = $result; Details = ($description + $hostName) }
        }
    }

    Write-Progress -Activity "Connection Check" -Status "Showing results" -Completed

    Format-ResultObject $connectionCheck
}

# P2P Check:
function Invoke-P2PHealthChecker()
{
    Print-Title " P2P Health, Errors, Configuration:"
    Write-Host ""
    Print-SubTitle "Peer Validation"

    $peerEfficiency = Check-PeerEfficiency
    Write-Host "`n$($peerEfficiency.Peer_Info) $($peerEfficiency.Description)"

    #***** Check Errors Found *****#
    Write-Host ""
    if($PSVersionTable.PSVersion.Major -lt 7) { Write-Host "" } # Adding an extra breakline in PS5 to keep the pattern of the next header

    Print-SubTitle "Errors Found (excluding transient errors)"
    $errorsFound = Get-PeerLogErrors
    if($errorsFound)
    {
        $errorsFound | Format-Table -Wrap -Autosize -Property @{Label='Error Code'; e={"0x{0:X}" -f $_.ErrorCode}}, Description
    }
    else
    {
        Write-Host " No errors Found!"
    }

    #***** Get DOPolicies *****#

    Print-SubTitle "Policy Settings"
    Get-DOPolicies -ErrorsFound $errorsFound | Format-Table -Wrap -Property @{Label='Name'; e={ "$($_.Name) " } ; Align='Left'; },
    @{Label='Configuration'; e={ if ($_.Configuration) { " $($_.Configuration) " } else { " Not Set " } }; Align='Center' ; },
    @{Label='More Info'; e={ if ($_.PolicySuggestion) { $color = "93"; $e = [char]27; "$e[${color}m$($_.MoreInfo)${e}[0m" } else { $_.MoreInfo } } ; }
}

# MCC Check:
function Invoke-MCCHealthChecker()
{
    Print-Title " MCC Device Setup:"

    $mccCheck = @()
    $mccCheck += Check-ConfiguredCacheHostServer
    $mccCheck += Check-DownloadCacheHostServer -CacheHost $mccCheck[0].MCCHost
    Format-ResultObject $mccCheck

    Print-Title " MCC Results on this Device:"
    $mccResults = @()
    $mccResults += Check-DownloadPercentageCacheHost
    $mccResults += Check-DelayCacheServerFallbackPolicy -PolicyName "DODelayCacheServerFallbackForeground" -RecommendedValue 30
    $mccResults += Check-DelayCacheServerFallbackPolicy -PolicyName "DODelayCacheServerFallbackBackground" -RecommendedValue 90
    Format-ResultObject $mccResults
}

#----------------------------------------------------------------------------------#
# MAIN SCRIPT:

$admin = Check-AdminPrivileges($MyInvocation.Line)
if (!$admin) { return }

#----------------------------------------------------------------------------------#
# Version number is specified in the metadata at the top of this file. This allows PS Gallery to consume it automatically.
# Use the metadata section to also display the version here.
# The version numbers must follow the rules of semantic versioning. See here for more details: https://semver.org/
$versionLine = Get-Content $MyInvocation.MyCommand.Definition -TotalCount 6 | Select-String '.VERSION'
$version = [Version]($versionLine -split ' ')[1]
Write-Host "Version $version"

$doConfig = Get-DOConfig -Verbose

#***** WinRT API (PS5 and PS7) *****#
$burntToastPreInstalled = $null
$moduleName = "BurntToast"
$onlyHealthCheck = ($HealthCheck -and !$P2P -and !$MCC)

if (-not $onlyHealthCheck)
{
    if($PSVersionTable.PSVersion.Major -gt 6)
    {
        $burntToastPreInstalled = Check-ModuleIsInstalled $moduleName

        if ($burntToastPreInstalled -eq $false)
        {
            Load-Module -Module $moduleName
        }
    }

    Import-Winrt
}
# ------------------------------- #

Print-OSInfo

if (!$HealthCheck -and !$P2P -and !$MCC)
{
    Invoke-HealthChecker
    Invoke-P2PHealthChecker
    Invoke-MCCHealthChecker
}
else
{
    if ($HealthCheck) { Invoke-HealthChecker }
    if ($P2P){ Invoke-P2PHealthChecker }
    if ($MCC){ Invoke-MCCHealthChecker }
}

#***** Remove Burnt Toast if it wasn't installed before in PS7 *****#
if ($burntToastPreInstalled -eq $false)
{
    Uninstall-Module -Name $moduleName -Force -WarningAction SilentlyContinue
}