param(
    [Parameter(Mandatory = $true)]
    [string]$InstallerPath,
    [string]$ValidationRoot = 'C:\HyperTwistInstallerValidation',
    [string]$ReportPath = '',
    [int]$LaunchSmokeSeconds = 20,
    [switch]$KeepInstalled
)

$ErrorActionPreference = 'Stop'

function Write-Utf8JsonFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [object]$Value
    )

    $ParentPath = Split-Path -Parent $Path
    if (-not [string]::IsNullOrWhiteSpace($ParentPath))
    {
        New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
    }

    $Json = $Value | ConvertTo-Json -Depth 12
    $Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}

function Invoke-InstallerProcess {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ExecutablePath,
        [Parameter(Mandatory = $true)]
        [string[]]$Arguments,
        [Parameter(Mandatory = $true)]
        [string]$Label
    )

    $Process = Start-Process `
        -FilePath $ExecutablePath `
        -ArgumentList $Arguments `
        -PassThru `
        -Wait
    if ($Process.ExitCode -notin @(0, 3010))
    {
        throw "$Label failed with exit code $($Process.ExitCode)."
    }

    return [int]$Process.ExitCode
}

function Invoke-InstallerProcessForExitCode {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ExecutablePath,
        [Parameter(Mandatory = $true)]
        [string[]]$Arguments
    )

    $Process = Start-Process `
        -FilePath $ExecutablePath `
        -ArgumentList $Arguments `
        -PassThru `
        -Wait
    return [int]$Process.ExitCode
}

function Resolve-PayloadRelativeFilePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath,
        [Parameter(Mandatory = $true)]
        [string]$RelativePath
    )

    if ([string]::IsNullOrWhiteSpace($RelativePath))
    {
        throw 'A payload-relative path cannot be empty.'
    }

    $WindowsRelativePath = $RelativePath.Replace('/', '\')
    if ([System.IO.Path]::IsPathRooted($WindowsRelativePath))
    {
        throw "Payload-relative path '$RelativePath' must not be rooted."
    }

    $ResolvedRootPath = [System.IO.Path]::GetFullPath($RootPath)
    $RootPrefix = $ResolvedRootPath.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
    $ResolvedFilePath = [System.IO.Path]::GetFullPath(
        (Join-Path $ResolvedRootPath $WindowsRelativePath)
    )
    if (-not $ResolvedFilePath.StartsWith(
        $RootPrefix,
        [System.StringComparison]::OrdinalIgnoreCase))
    {
        throw "Payload-relative path '$RelativePath' escapes root '$RootPath'."
    }

    return $ResolvedFilePath
}

function Test-PayloadManifest {
    param(
        [Parameter(Mandatory = $true)]
        [string]$InstallRoot,
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath
    )

    $Manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
    $Failures = New-Object System.Collections.ArrayList
    $ExpectedPaths = @{}
    foreach ($Entry in @($Manifest.files))
    {
        $EntryRelativePath = [string]$Entry.relativePath
        $EntryPathKey = $EntryRelativePath.Replace('\', '/').ToLowerInvariant()
        if ($ExpectedPaths.ContainsKey($EntryPathKey))
        {
            [void]$Failures.Add("duplicate:$EntryRelativePath")
            continue
        }
        $ExpectedPaths[$EntryPathKey] = $true

        try
        {
            $InstalledPath = Resolve-PayloadRelativeFilePath `
                -RootPath $InstallRoot `
                -RelativePath $EntryRelativePath
        }
        catch
        {
            [void]$Failures.Add("invalid-path:$EntryRelativePath")
            continue
        }

        if (-not (Test-Path -LiteralPath $InstalledPath -PathType Leaf))
        {
            [void]$Failures.Add("missing:$EntryRelativePath")
            continue
        }

        $InstalledItem = Get-Item -LiteralPath $InstalledPath
        if ($InstalledItem.Length -ne [Int64]$Entry.sizeBytes)
        {
            [void]$Failures.Add("size:$EntryRelativePath")
            continue
        }

        $InstalledHash = (Get-FileHash -LiteralPath $InstalledPath -Algorithm SHA256).Hash.ToLowerInvariant()
        if ($InstalledHash -ne ([string]$Entry.sha256).ToLowerInvariant())
        {
            [void]$Failures.Add("sha256:$EntryRelativePath")
        }
    }

    $AllowedInstallerPaths = @{}
    @(
        'Installer/payload-manifest.json',
        'Installer/Test-HyperTwistWindowsReadiness.ps1',
        'Installer/HyperTwist.install-marker',
        'Uninstall HyperTwist.exe'
    ) | ForEach-Object { $AllowedInstallerPaths[$_.ToLowerInvariant()] = $true }
    $InstallRootPrefix = [System.IO.Path]::GetFullPath($InstallRoot).TrimEnd('\') + '\'
    $UnexpectedFiles = @(
        Get-ChildItem -LiteralPath $InstallRoot -File -Force -Recurse |
            ForEach-Object {
                $RelativePath = $_.FullName.Substring($InstallRootPrefix.Length).Replace('\', '/')
                $RelativePathKey = $RelativePath.ToLowerInvariant()
                if (-not $ExpectedPaths.ContainsKey($RelativePathKey) `
                    -and -not $AllowedInstallerPaths.ContainsKey($RelativePathKey))
                {
                    $RelativePath
                }
            }
    )
    $UnexpectedPdbFiles = @(
        Get-ChildItem -LiteralPath $InstallRoot -Filter '*.pdb' -File -Recurse -ErrorAction SilentlyContinue
    )
    return [pscustomobject][ordered]@{
        passed = $Failures.Count -eq 0 `
            -and $UnexpectedPdbFiles.Count -eq 0 `
            -and $UnexpectedFiles.Count -eq 0
        manifestEntryCount = @($Manifest.files).Count
        failureCount = $Failures.Count
        failures = @($Failures)
        unexpectedFiles = $UnexpectedFiles
        unexpectedPdbFiles = @($UnexpectedPdbFiles | Select-Object -ExpandProperty FullName)
    }
}

function Get-OwnedProcessRecords {
    param(
        [Parameter(Mandatory = $true)]
        [string]$InstallRoot
    )

    $InstallPrefix = $InstallRoot.TrimEnd('\') + '\'
    return @(
        Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
            Where-Object {
                -not [string]::IsNullOrWhiteSpace([string]$_.ExecutablePath) `
                    -and ([string]$_.ExecutablePath).StartsWith(
                        $InstallPrefix,
                        [System.StringComparison]::OrdinalIgnoreCase)
            }
    )
}

function Stop-OwnedProcesses {
    param(
        [Parameter(Mandatory = $true)]
        [string]$InstallRoot
    )

    $StoppedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
    for ($Attempt = 0; $Attempt -lt 8; $Attempt += 1)
    {
        $OwnedProcesses = @(Get-OwnedProcessRecords -InstallRoot $InstallRoot)
        if ($OwnedProcesses.Count -eq 0)
        {
            break
        }

        foreach ($OwnedProcess in $OwnedProcesses)
        {
            Stop-Process -Id ([int]$OwnedProcess.ProcessId) -Force -ErrorAction SilentlyContinue
            [void]$StoppedProcessIds.Add([int]$OwnedProcess.ProcessId)
        }
        Start-Sleep -Milliseconds 250
    }

    return @($StoppedProcessIds)
}

function Invoke-InstalledLaunchSmoke {
    param(
        [Parameter(Mandatory = $true)]
        [string]$InstallRoot,
        [Parameter(Mandatory = $true)]
        [string]$EvidenceDirectory,
        [int]$SmokeSeconds
    )

    $ExecutablePath = Join-Path $InstallRoot 'UnrealHyperTwist.exe'
    $RuntimeLogPath = Join-Path $EvidenceDirectory 'installed-runtime.log'
    $DiagnosticsPath = Join-Path $EvidenceDirectory 'installed-runtime.hyperdiagnostics.log'
    $UserDirectory = Join-Path $EvidenceDirectory 'isolated-user'
    New-Item -ItemType Directory -Force -Path $UserDirectory | Out-Null

    $Arguments = @(
        '-NullRHI',
        '-nosound',
        '-windowed',
        '-ResX=1280',
        '-ResY=720',
        '-log',
        '-FORCELOGFLUSH',
        "-abslog=$RuntimeLogPath",
        "-HyperTwistDiagnosticsLog=`"$DiagnosticsPath`"",
        "-UserDir=`"$UserDirectory`""
    )
    $StartedAtUtc = [DateTime]::UtcNow
    $LaunchProcess = Start-Process `
        -FilePath $ExecutablePath `
        -ArgumentList $Arguments `
        -PassThru
    Start-Sleep -Seconds $SmokeSeconds

    $OwnedProcesses = @(Get-OwnedProcessRecords -InstallRoot $InstallRoot)
    $OwnedProcessIds = @($OwnedProcesses | Select-Object -ExpandProperty ProcessId)
    $ListeningEndpoints = @(
        if ($OwnedProcessIds.Count -gt 0)
        {
            Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
                Where-Object { $OwnedProcessIds -contains [int]$_.OwningProcess } |
                ForEach-Object {
                    [pscustomobject][ordered]@{
                        processId = [int]$_.OwningProcess
                        localAddress = [string]$_.LocalAddress
                        localPort = [int]$_.LocalPort
                    }
                }
        }
    )
    $FatalPatterns = @(
        'Fatal error:',
        'LowLevelFatalError',
        'Assertion failed:',
        'Critical error:',
        'Unhandled Exception:',
        'StaticShutdownAfterError'
    )
    $RuntimeLines = @(
        if (Test-Path -LiteralPath $RuntimeLogPath)
        {
            Get-Content -LiteralPath $RuntimeLogPath
        }
    )
    $DiagnosticsLines = @(
        if (Test-Path -LiteralPath $DiagnosticsPath)
        {
            Get-Content -LiteralPath $DiagnosticsPath
        }
    )
    $FatalLines = @(
        @($RuntimeLines + $DiagnosticsLines) |
            Where-Object {
                $Line = $_.ToString()
                @($FatalPatterns | Where-Object { $Line -like "*$_*" }).Count -gt 0
            }
    )
    $Initialized = @(
        $DiagnosticsLines |
            Where-Object {
                $_.ToString().IndexOf(
                    '[Process] HyperTwist runtime module initialized.',
                    [System.StringComparison]::OrdinalIgnoreCase
                ) -ge 0
            }
    ).Count -gt 0

    $StoppedProcessIds = @(Stop-OwnedProcesses -InstallRoot $InstallRoot)
    $Passed = $OwnedProcessIds.Count -gt 0 `
        -and (Test-Path -LiteralPath $DiagnosticsPath -PathType Leaf) `
        -and $Initialized `
        -and $FatalLines.Count -eq 0 `
        -and @($ListeningEndpoints | Where-Object { $_.localPort -eq 1985 }).Count -eq 0
    return [pscustomobject][ordered]@{
        passed = $Passed
        startedAtUtc = $StartedAtUtc.ToString('o')
        launcherProcessId = [int]$LaunchProcess.Id
        ownedProcessIds = $OwnedProcessIds
        stoppedProcessIds = $StoppedProcessIds
        runtimeLogPath = $RuntimeLogPath
        diagnosticsPath = $DiagnosticsPath
        diagnosticsInitialized = $Initialized
        fatalLines = $FatalLines
        listeningEndpoints = $ListeningEndpoints
    }
}

function Get-ShortcutEvidence {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf))
    {
        return [pscustomobject][ordered]@{
            path = $Path
            exists = $false
            targetPath = $null
            arguments = $null
        }
    }

    $Shell = New-Object -ComObject WScript.Shell
    $Shortcut = $null
    try
    {
        $Shortcut = $Shell.CreateShortcut($Path)
        return [pscustomobject][ordered]@{
            path = $Path
            exists = $true
            targetPath = [string]$Shortcut.TargetPath
            arguments = [string]$Shortcut.Arguments
        }
    }
    finally
    {
        if ($null -ne $Shortcut)
        {
            [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($Shortcut)
        }
        [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($Shell)
    }
}

$CurrentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
$CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal($CurrentIdentity)
if (-not $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
{
    throw 'Installer lifecycle validation requires an elevated Windows session.'
}
if (-not (Test-Path -LiteralPath $InstallerPath -PathType Leaf))
{
    throw "Installer '$InstallerPath' was not found."
}

$ResolvedInstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
$ResolvedValidationRoot = [System.IO.Path]::GetFullPath($ValidationRoot)
$RunId = [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ')
$RunDirectory = Join-Path $ResolvedValidationRoot $RunId
$InstallRoot = Join-Path $RunDirectory 'HyperTwist'
$EvidenceDirectory = Join-Path $RunDirectory 'evidence'
New-Item -ItemType Directory -Force -Path $EvidenceDirectory | Out-Null
if ([string]::IsNullOrWhiteSpace($ReportPath))
{
    $ReportPath = Join-Path $EvidenceDirectory 'windows-installer-lifecycle-report.json'
}

$RepositoryRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$ReadinessScriptPath = Join-Path $RepositoryRoot 'scripts\Test-HyperTwistWindowsReadiness.ps1'
$UninstallRegistryPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\HyperTwist'
$ProductRegistryPath = 'HKLM:\SOFTWARE\HyperTwist\HyperTwist'
$CommonProgramsPath = [Environment]::GetFolderPath('CommonPrograms')
$CommonDesktopPath = [Environment]::GetFolderPath('CommonDesktopDirectory')
$StartMenuDirectory = Join-Path $CommonProgramsPath 'HyperTwist'
$DesktopShortcutPath = Join-Path $CommonDesktopPath 'HyperTwist.lnk'

if (Test-Path -LiteralPath $UninstallRegistryPath)
{
    throw 'A registered HyperTwist installation already exists; validation will not replace it.'
}

$InstallerItem = Get-Item -LiteralPath $ResolvedInstallerPath
$Report = [ordered]@{
    reportVersion = 'ht-windows-installer-lifecycle/v1'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    result = 'failed'
    validationClass = 'isolated-current-host'
    cleanMachineAvailable = $false
    cleanMachineBoundary = 'No Windows Sandbox, clean Hyper-V VM, or base VHD was available on the validation host.'
    installerPath = $ResolvedInstallerPath
    installerSizeBytes = [Int64]$InstallerItem.Length
    installerSha256 = (Get-FileHash -LiteralPath $ResolvedInstallerPath -Algorithm SHA256).Hash.ToLowerInvariant()
    installerSignatureStatus = (Get-AuthenticodeSignature -LiteralPath $ResolvedInstallerPath).Status.ToString()
    installRoot = $InstallRoot
    unownedNonEmptyRefusal = $null
    firstInstallExitCode = $null
    upgradeInstallExitCode = $null
    payloadParityAfterFirstInstall = $null
    payloadParityAfterUpgrade = $null
    readiness = $null
    launchSmoke = $null
    uninstallExitCode = $null
    registrationVerified = $false
    shortcutsVerified = $false
    shortcutDetails = $null
    userDataPreserved = $false
    installRootRemoved = $false
    registrationRemoved = $false
    shortcutsRemoved = $false
    cleanupCompleted = $false
    error = $null
}

$UninstallerPath = Join-Path $InstallRoot 'Uninstall HyperTwist.exe'
$SentinelDirectory = Join-Path $env:LOCALAPPDATA 'UnrealHyperTwist\Saved\InstallerValidation'
$SentinelPath = Join-Path $SentinelDirectory "$RunId.txt"
$Installed = $false

try
{
    New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
    $UnownedSentinelPath = Join-Path $InstallRoot 'unrelated-owner-sentinel.txt'
    [System.IO.File]::WriteAllText(
        $UnownedSentinelPath,
        "Unowned install-root refusal sentinel $RunId",
        (New-Object System.Text.UTF8Encoding($false))
    )
    $UnownedRefusalExitCode = Invoke-InstallerProcessForExitCode `
        -ExecutablePath $ResolvedInstallerPath `
        -Arguments @('/S', "/D=$InstallRoot")
    if ($UnownedRefusalExitCode -eq 0)
    {
        $Installed = Test-Path -LiteralPath $UninstallerPath -PathType Leaf
    }
    $Report.unownedNonEmptyRefusal = [pscustomobject][ordered]@{
        passed = $UnownedRefusalExitCode -eq 14 `
            -and (Test-Path -LiteralPath $UnownedSentinelPath -PathType Leaf) `
            -and -not (Test-Path -LiteralPath $UninstallRegistryPath) `
            -and -not (Test-Path -LiteralPath $ProductRegistryPath)
        exitCode = $UnownedRefusalExitCode
        expectedExitCode = 14
        sentinelPreserved = Test-Path -LiteralPath $UnownedSentinelPath -PathType Leaf
        registrationAbsent = `
            -not (Test-Path -LiteralPath $UninstallRegistryPath) `
            -and -not (Test-Path -LiteralPath $ProductRegistryPath)
    }
    if (-not $Report.unownedNonEmptyRefusal.passed)
    {
        throw 'The installer did not safely refuse an unowned non-empty installation root.'
    }
    Remove-Item -LiteralPath $InstallRoot -Recurse -Force

    $Report.firstInstallExitCode = Invoke-InstallerProcess `
        -ExecutablePath $ResolvedInstallerPath `
        -Arguments @('/S', "/D=$InstallRoot") `
        -Label 'Silent installer'
    $Installed = $true

    $ManifestPath = Join-Path $InstallRoot 'Installer\payload-manifest.json'
    if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf))
    {
        throw 'The installed payload manifest is missing.'
    }
    $Report.payloadParityAfterFirstInstall = Test-PayloadManifest `
        -InstallRoot $InstallRoot `
        -ManifestPath $ManifestPath
    if (-not $Report.payloadParityAfterFirstInstall.passed)
    {
        throw 'Installed payload parity failed after first install.'
    }

    $RegisteredProduct = Get-ItemProperty -LiteralPath $UninstallRegistryPath
    $RegisteredOwner = Get-ItemProperty -LiteralPath $ProductRegistryPath
    $Report.registrationVerified = `
        [System.IO.Path]::GetFullPath([string]$RegisteredProduct.InstallLocation) -eq $InstallRoot `
        -and [System.IO.Path]::GetFullPath([string]$RegisteredOwner.InstallLocation) -eq $InstallRoot
    if (-not $Report.registrationVerified)
    {
        throw 'Add/Remove Programs or product registration does not match the isolated install root.'
    }

    $ExpectedApplicationPath = Join-Path $InstallRoot 'UnrealHyperTwist.exe'
    $ExpectedReadinessScriptPath = Join-Path $InstallRoot 'Installer\Test-HyperTwistWindowsReadiness.ps1'
    $ExpectedManifestPath = Join-Path $InstallRoot 'Installer\payload-manifest.json'
    $ExpectedUninstallerPath = Join-Path $InstallRoot 'Uninstall HyperTwist.exe'
    $ApplicationShortcut = Get-ShortcutEvidence `
        -Path (Join-Path $StartMenuDirectory 'HyperTwist.lnk')
    $ReadinessShortcut = Get-ShortcutEvidence `
        -Path (Join-Path $StartMenuDirectory 'System Readiness Report.lnk')
    $UninstallShortcut = Get-ShortcutEvidence `
        -Path (Join-Path $StartMenuDirectory 'Uninstall HyperTwist.lnk')
    $DesktopShortcut = Get-ShortcutEvidence -Path $DesktopShortcutPath
    $Report.shortcutDetails = [pscustomobject][ordered]@{
        application = $ApplicationShortcut
        readiness = $ReadinessShortcut
        uninstall = $UninstallShortcut
        desktop = $DesktopShortcut
    }
    $Report.shortcutsVerified = `
        $ApplicationShortcut.exists `
        -and $ApplicationShortcut.targetPath -eq $ExpectedApplicationPath `
        -and $ReadinessShortcut.exists `
        -and $ReadinessShortcut.targetPath.EndsWith(
            '\WindowsPowerShell\v1.0\powershell.exe',
            [System.StringComparison]::OrdinalIgnoreCase
        ) `
        -and $ReadinessShortcut.arguments.Contains($ExpectedReadinessScriptPath) `
        -and $ReadinessShortcut.arguments.Contains($ExpectedManifestPath) `
        -and $ReadinessShortcut.arguments.Contains('-WaitForUser') `
        -and $UninstallShortcut.exists `
        -and $UninstallShortcut.targetPath -eq $ExpectedUninstallerPath `
        -and $DesktopShortcut.exists `
        -and $DesktopShortcut.targetPath -eq $ExpectedApplicationPath
    if (-not $Report.shortcutsVerified)
    {
        throw 'One or more installer-managed shortcuts are missing or have incorrect targets or arguments.'
    }

    $ReadinessReportPath = Join-Path $EvidenceDirectory 'installed-readiness-report.json'
    $Report.readiness = & $ReadinessScriptPath `
        -PayloadRoot $InstallRoot `
        -TargetInstallRoot $InstallRoot `
        -ExpectedPayloadManifestPath $ManifestPath `
        -ReportPath $ReadinessReportPath `
        -VerifyPayloadHashes `
        -PassThru
    if ($Report.readiness.result -eq 'blocked')
    {
        throw 'Installed-host readiness validation is blocked.'
    }

    New-Item -ItemType Directory -Force -Path $SentinelDirectory | Out-Null
    [System.IO.File]::WriteAllText(
        $SentinelPath,
        "HyperTwist installer validation sentinel $RunId",
        (New-Object System.Text.UTF8Encoding($false))
    )

    $Report.launchSmoke = Invoke-InstalledLaunchSmoke `
        -InstallRoot $InstallRoot `
        -EvidenceDirectory $EvidenceDirectory `
        -SmokeSeconds $LaunchSmokeSeconds
    if (-not $Report.launchSmoke.passed)
    {
        throw 'Installed executable launch smoke failed.'
    }

    $Report.upgradeInstallExitCode = Invoke-InstallerProcess `
        -ExecutablePath $ResolvedInstallerPath `
        -Arguments @('/S', "/D=$InstallRoot") `
        -Label 'Silent in-place upgrade'
    $ManifestPath = Join-Path $InstallRoot 'Installer\payload-manifest.json'
    $Report.payloadParityAfterUpgrade = Test-PayloadManifest `
        -InstallRoot $InstallRoot `
        -ManifestPath $ManifestPath
    if (-not $Report.payloadParityAfterUpgrade.passed)
    {
        throw 'Installed payload parity failed after in-place upgrade.'
    }

    if (-not $KeepInstalled)
    {
        $Report.uninstallExitCode = Invoke-InstallerProcess `
            -ExecutablePath $UninstallerPath `
            -Arguments @('/S') `
            -Label 'Silent uninstaller'
        $Installed = $false
        Start-Sleep -Seconds 3

        $Report.userDataPreserved = Test-Path -LiteralPath $SentinelPath -PathType Leaf
        $Report.installRootRemoved = -not (Test-Path -LiteralPath $InstallRoot)
        $Report.registrationRemoved = `
            -not (Test-Path -LiteralPath $UninstallRegistryPath) `
            -and -not (Test-Path -LiteralPath $ProductRegistryPath)
        $Report.shortcutsRemoved = `
            -not (Test-Path -LiteralPath $StartMenuDirectory) `
            -and -not (Test-Path -LiteralPath $DesktopShortcutPath)

        if (-not $Report.userDataPreserved)
        {
            throw 'Default uninstall removed current-user data unexpectedly.'
        }
        if (-not $Report.installRootRemoved)
        {
            throw 'The isolated install root remains after uninstall.'
        }
        if (-not $Report.registrationRemoved)
        {
            throw 'Installer registry state remains after uninstall.'
        }
        if (-not $Report.shortcutsRemoved)
        {
            throw 'Installer-managed shortcuts remain after uninstall.'
        }
    }

    $Report.cleanupCompleted = -not $KeepInstalled
    $Report.result = 'passed'
}
catch
{
    $Report.error = $_.Exception.Message
    throw
}
finally
{
    [void](Stop-OwnedProcesses -InstallRoot $InstallRoot)
    if ($Installed -and -not $KeepInstalled -and (Test-Path -LiteralPath $UninstallerPath -PathType Leaf))
    {
        try
        {
            [void](Invoke-InstallerProcess `
                -ExecutablePath $UninstallerPath `
                -Arguments @('/S') `
                -Label 'Failure-cleanup uninstaller')
            Start-Sleep -Seconds 3
            $Installed = $false
        }
        catch
        {
            if ([string]::IsNullOrWhiteSpace([string]$Report.error))
            {
                $Report.error = $_.Exception.Message
            }
            else
            {
                $Report.error = "$($Report.error) | Cleanup: $($_.Exception.Message)"
            }
        }
    }

    if (-not $KeepInstalled `
        -and (Test-Path -LiteralPath $InstallRoot) `
        -and -not (Test-Path -LiteralPath $UninstallRegistryPath) `
        -and -not (Test-Path -LiteralPath $ProductRegistryPath))
    {
        Remove-Item -LiteralPath $InstallRoot -Recurse -Force
    }

    if (Test-Path -LiteralPath $SentinelPath)
    {
        Remove-Item -LiteralPath $SentinelPath -Force
    }
    if ((Test-Path -LiteralPath $SentinelDirectory) `
        -and @(Get-ChildItem -LiteralPath $SentinelDirectory -Force).Count -eq 0)
    {
        Remove-Item -LiteralPath $SentinelDirectory -Force
    }

    if (-not $KeepInstalled)
    {
        $Report.installRootRemoved = -not (Test-Path -LiteralPath $InstallRoot)
        $Report.registrationRemoved = `
            -not (Test-Path -LiteralPath $UninstallRegistryPath) `
            -and -not (Test-Path -LiteralPath $ProductRegistryPath)
        $Report.shortcutsRemoved = `
            -not (Test-Path -LiteralPath $StartMenuDirectory) `
            -and -not (Test-Path -LiteralPath $DesktopShortcutPath)
        $Report.cleanupCompleted = `
            -not $Installed `
            -and $Report.installRootRemoved `
            -and $Report.registrationRemoved `
            -and $Report.shortcutsRemoved
    }

    Write-Utf8JsonFile -Path $ReportPath -Value $Report
}

[Console]::Out.WriteLine('HyperTwist Windows installer lifecycle validation passed.')
[Console]::Out.WriteLine("Report: $([System.IO.Path]::GetFullPath($ReportPath))")
