<#
.SYNOPSIS
  Tester - Automatic specification gathering, testing, and configuration tool.

.DESCRIPTION
  Performs automated provisioning tasks including:
   - Smart Windows Update with reboot resume
   - GPU detection and discrete driver installation
   - Hardware specification gathering
   - OEM BIOS configuration (HP, Dell, Lenovo)
   - BurnInTest automation
   - Secure erase tooling
   - Cleanup

.PARAMETER Help
  Display usage information.

.PARAMETER LogFile
  Path to a .log file for transcript logging.

.PARAMETER QROnly
  Only retrieve system specifications as a QR code.

.PARAMETER BiosOnly
  Only handle BIOS for Lenovo, HP, and Dell systems.

.PARAMETER TestOnly
  Only install and run BurnInTest.

.PARAMETER EraseOnly
  Only install and run the data eraser.

.PARAMETER Deprecated
  Use handwritten specs like a pleb.

.NOTES
  Author: Josh Ashton
  Email: joshua.ashton@npsstore.com
  Secondary Email: me@joshashton.dev
  Last Modified: 2026-04-20

.CHANGELOG
  v0.0.1  Initial version (HP BIOS)
  v0.0.2  Added Dell BIOS, resolution, BurnInTest
  v0.0.3  Fixed resolution bug, added cleanup
  v0.0.4  Improved cleanup & logging
  v0.0.5  Fixed Dell BIOS logic
  v0.0.6  Logging improvements
  v0.0.7  Lenovo BIOS password detection and GPU-aware Windows Update with reboot resume
  v0.0.8  Added BiosOnly flag.
  v0.0.9  Updated SpecOnly to SpecsOnly, and added DDR5 PartNumber to Specs.
  v0.1.0  Updated Get-Drives to include drive power on hours.
  v0.2.0  Data transfer to NPS computer via QR code and Google Chrome extension for auto-filling Pricer.
  v0.2.1  Modified SpecsOnly flag to QROnly, added automatic chassis detection to include screen info.
  v0.2.2  Feature removed - drive power on hours as it's generally N/A.
  v0.2.3  Added output to console with device specs.
#>

param(
  [switch]$Help,
  [switch]$QROnly,
  [switch]$BiosOnly,
  [switch]$TestOnly,
  [switch]$EraseOnly,
  [switch]$Deprecated,

  [ValidateScript({
    if ($_ -notmatch "\.log$") {
      throw "LogFile must be a .log file"
    }
    $true
  })]
  [System.IO.FileInfo]$LogFile = "C:\Users\Administrator\Desktop\nps.log"
)

# ==================================================
# Configuration
# ==================================================
$RegPath = "HKLM:\SOFTWARE\NPS\Provisioning"
$RegName = "WindowsUpdated"

# ==================================================
# Registry helpers
# ==================================================
function Get-UpdateFlag {
  if (Test-Path $RegPath) {
    (Get-ItemProperty -Path $RegPath -Name $RegName -ErrorAction SilentlyContinue).$RegName
  }
}

function Set-UpdateFlag {
  if (-not (Test-Path $RegPath)) {
    New-Item -Path $RegPath -Force | Out-Null
  }
  Set-ItemProperty -Path $RegPath -Name $RegName -Value 1 -Type DWord
}

function Clear-UpdateFlag {
  if (Test-Path $RegPath) {
    Remove-Item -Path $RegPath -Recurse -Force
  }
}

# ==================================================
# Windows Update
# ==================================================
function Update-Windows {
  Install-Module PSWindowsUpdate -Force -Confirm:$false
  Import-Module PSWindowsUpdate
  Get-WindowsUpdate -MicrosoftUpdate -Install -AcceptAll -IgnoreReboot

  Set-UpdateFlag
  Restart-Computer -Force
  exit
}

# ==================================================
# GPU handling
# ==================================================
function Install-Nvidia {
  Invoke-WebRequest -Uri "https://us6-dl.techpowerup.com/files/9op87Lz6fZ9K3oUZLZlWxg/1769245820/NVCleanstall_1.19.0.exe" -Outfile "C:\Users\Administrator\Desktop\NVCleanstall.exe"

  Start-Process "C:\Users\Administrator\Desktop\NVCleanstall.exe" -Wait
}

function Install-AMD {

}

function Handle-GPU {
  $Adapters = @(Get-CimInstance Win32_VideoController |
                Select-Object -ExpandProperty Name)

  $Updated = Get-UpdateFlag

  if (-not $Updated) {
    if ($Adapters.Count -ge 1 -and
        "Microsoft Basic Display Adapter" -In $Adapters) {

      Write-Host "Basic Display Adapter detected. Running Windows Update."
      Update-Windows
    }
    return
  }

  if ($Updated -and $Adapters.Count -ge 2 -and ("Microsoft Basic Display Adapter" -In $Adapters)) {
    Write-Host "Second adapter still Basic Display Adapter. Discrete GPU detected."

    $GpuDevices = Get-PnpDevice -Class Display -Status OK
    foreach ($gpu in $GpuDevices) {
      $ids = (Get-PnpDeviceProperty `
        -InstanceId $gpu.InstanceId `
        -KeyName 'DEVPKEY_Device_HardwareIds' `
        -ErrorAction SilentlyContinue).Data

      if ($ids -match 'VEN_10DE') { Install-Nvidia }
      elseif ($ids -match 'VEN_1002') { Write-Host "Automated installation of AMD Radeon Graphics drivers is not supported yet!" -ForegroundColor Yellow }
    }
  }

  Clear-UpdateFlag
}

# ==================================================
# Specs
# ==================================================
function Get-RAM {
    # Sum the capacity of all physical sticks to get the "Marketing" total
    $PhysicalMem = Get-CimInstance Win32_PhysicalMemory
    $RawSize = ($PhysicalMem | Measure-Object -Property Capacity -Sum).Sum
    $Size = [Math]::Round($RawSize / 1GB)
    
    # Get the first stick for metadata
    $MemStick = $PhysicalMem | Select-Object -First 1
    $Type = $MemStick.SMBIOSMemoryType
    $PartNumber = $MemStick.PartNumber.Trim()

    $TypeStr = switch ($Type) {
        34 { "DDR5" }
        26 { "DDR4" }
        24 { "DDR3" }
        default { "DDR" }
    }

    if ($TypeStr -eq "DDR5") {
        return "$Size GB $TypeStr ($PartNumber)"
    } else {
        return "$Size GB $TypeStr"
    }
}

function Get-CDDrive {
  $cd = Get-CimInstance Win32_CDROMDrive
  if (-not $cd) { "---" }
  elseif ($cd.CanWrite) { "Read/Write" }
  else { "Read-only" }
}

function Get-Drives {
    $PhysicalDisks = Get-PhysicalDisk | Where-Object { $_.BusType -ne 'USB' } |
        Select-Object DeviceId, SerialNumber, BusType, MediaType, Size

    return $PhysicalDisks | Select-Object `
        DeviceId, 
        BusType, 
        MediaType,
        @{Name="Size"; Expression={
            $rawGB = $_.Size / 1GB
            if ($rawGB -gt 900 -and $rawGB -lt 1024) { 1000 }
            elseif ($rawGB -gt 470 -and $rawGB -lt 512) { 512 }
            elseif ($rawGB -gt 450 -and $rawGB -lt 500) { 500 }
            elseif ($rawGB -gt 230 -and $rawGB -lt 256) { 256 }
            elseif ($rawGB -gt 115 -and $rawGB -lt 128) { 128 }
            elseif ($rawGB -gt 100 -and $rawGB -lt 115) { 128 }
            else { [Math]::Round($rawGB) }
        }}
}

function Get-Resolution {
  Add-Type -AssemblyName System.Windows.Forms
  $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
  "$($b.Width)x$($b.Height)"
}

function Get-Specs {
  Handle-GPU

  $Model = (Get-CimInstance Win32_ComputerSystem).Model
  $CPU = (Get-CimInstance Win32_Processor).Name
  $RAM = Get-RAM
  $CD = Get-CDDrive
  $GPU = (Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name) -join ", "
  $OS = (Get-CimInstance Win32_OperatingSystem).Caption
  $Res = Get-Resolution
  $Drives = Get-Drives

  Write-Host "`nSystem Information`n------------------" -ForegroundColor Cyan
  Write-Host @"
Model: $Model
CPU: $CPU
RAM: $RAM
CD: $CD
GPU: $GPU
OS: $OS
Resolution: $Res
Drives:
"@

  $Drives | Format-Table -AutoSize
}

# ==================================================
# BIOS handling
# ==================================================
function Handle-BIOS {
  Write-Host "Configuring BIOS ..." -ForegroundColor Yellow
  $Maker = (Get-CimInstance Win32_ComputerSystem).Manufacturer

  switch ($Maker) {

    "HP" {
      Invoke-WebRequest -Uri "https://hpia.hpcloud.hp.com/downloads/cmsl/hp-cmsl-1.8.5.exe" `
        -OutFile "C:\Users\Administrator\Desktop\hp.exe"

      Start-Process "C:\Users\Administrator\Desktop\hp.exe" -Wait
      $env:Path += "C:\Program Files\WindowsPowerShell\Scripts\HP.ClientScriptLibrary;"

      if ((Get-HPBIOSSetupPasswordIsSet) -eq $false) {
        if ((Get-HPBIOSSetting -Name "Absolute Persistence Module Current State").Active -ne $false) {
          Set-HPBIOSSettingValue -Name "Permanent Disable Absolute Persistence Module Set Once" -Value "Yes"
          Write-Host "Absolute Persistence Module disabled." -ForegroundColor Green
        }
      }
      else { Write-Host "BIOS Admin and/or System password is set and cannot disable BIOS tracking! Attempt to clear password with hardware pins and reboot." -ForegroundColor Red }
    }

    "Dell Inc." {
      Install-Module DellBIOSProvider -Force
      Import-Module DellBIOSProvider

      if ((Get-Item DellSmbios:\Security\IsAdminPasswordSet) -and
          (Get-Item DellSmbios:\Security\IsSystemPasswordSet)) {

        Set-Item DellSmbios:\Security\Absolute "PermanentlyDisabled"
        Write-Host "Dell Absolute disabled." -ForegroundColor Green
      }
      else { Write-Host "BIOS Admin and/or System password is set and cannot disable BIOS tracking! Attempt to clear password with hardware pins and reboot." -ForegroundColor Red }
    }

    "Lenovo" {
      $PasswordState = (Get-WmiObject -Namespace root\wmi -Class Lenovo_BiosPasswordSettings).PasswordState
      
      switch ($PasswordState) {
        0 { Write-Host "No BIOS passwords are set! Enter BIOS settings manually to permanently disable Computrace/Absolute BIOS tracking." -ForegroundColor Green }
        1 { Write-Host "Power on Password is set! How did we get here ... Attempt to manually clear password with hardware pins." -ForegroundColor Red }
        2 { Write-Host "Supervisor password is set! Attempt to manually clear password with hardware pins." -ForegroundColor Red }
        3 { Write-Host "Power on and supervisor passwords are set! How did we get here ... Attempt to manually clear passwords with hardware pins." -ForegroundColor Red }
        4 { Write-Host "Hard drive password is set! How did we get here ... Replace the hard drive." -ForegroundColor Red }
        5 { Write-Host "Power on and hard drive passwords are set! Really, how did we get here? ... Attempt to manually clear password with hardware pins and replace the hard drive." -ForegroundColor Red }
        6 { Write-Host "Supervisor and hard drive passwords are set! How did we get here ... Attempt to manually clear password with hardware pins and replace the hard drive." -ForegroundColor Red }
        7 { Write-Host "Supervisor, power on, and hard drive passwords are set! Really, how did we get here? I'm in agony ... Attempt to manually clear password with hardware pins and replace the hard drive." -ForegroundColor Red }
        64 { Write-Host "System management password is set! Attempt to manually clear password with hardware pins, but it probably won't work ... PAIN" -ForegroundColor Red }
        65 { Write-Host "System management and power on passwords are set! How did we get here ... Attempt to manually clear password with hardware pins, but it probably won't work ... PAIN" -ForegroundColor Red }
        66 { Write-Host "System management and supervisor passwords are set! Execute Order 66 ... Attempt to manually clear password with hardware pins, but it probably won't work ... PAIN" -ForegroundColor Red }
        67 { Write-Host "System management, supervisor, and power on passwords are set! Really, how did we get here? I'm in agony ... Attempt to manually clear passwords with hardware pins and replace the hard drive, but it probably won't work ... PAIN" -ForegroundColor Red }
        68 { Write-Host "System management and hard drive passwords are set! How did we get here ... Attempt to manually clear password with hardware pins and replace the hard drive, but it probably won't work ... PAIN" -ForegroundColor Red }
        69 { Write-Host "System management, power on, and hard drive passwords are set! Nice code. How did we get here ... Attempt to manually clear password with hardware pins and replace the hard drive, but it probably won't work ... PAIN" -ForegroundColor Red }
        70 { Write-Host "System management, supervisor, and hard drive passwords are set! How did we get here ... Attempt to manually clear passwords with hardware pins and replace the hard drive, but it probably won't work ... PAIN" -ForegroundColor Red }
        71 { Write-Host "System management, supervisor, power on, and hard drive passwords are set! Really, how did we get here? I'm in agony ... Attempt to manually clear passwords with hardware pins and replace the hard drive, but it probably won't work ... PAIN" -ForegroundColor Red }
      }
    }
  }
}

# ==================================================
# BurnInTest / Eraser / Cleanup
# ==================================================
function Start-BurnInTest {
  Write-Host "Installing BurnInTest ..." -ForegroundColor Yellow
  Start-Process ".\BurnInTest_Windows_x86-64.exe" -Wait

  Write-Host "Running BurnInTest, press continue in the pop-up ..." -ForegroundColor Yellow
  Start-Process "C:\Program Files\BurnInTest\bit.exe" -ArgumentList "-R","-C",".\config.bitcfg" -Wait
}

function Start-Eraser {
  Write-Host "Installing Eraser ..." -ForegroundColor Yellow
  Start-Process ".\Eraser 6.2.0.2970.exe" -Wait

  Write-Host "Running Eraser. Once completed, exit through the System Tray." -ForegroundColor Yellow
  Start-Process "C:\Program Files\Eraser\Eraser.exe" -Wait
}

function Cleanup {
  Write-Host "Cleaning up..." -ForegroundColor Yellow
  $Maker = (Get-CimInstance Win32_ComputerSystem).Manufacturer

  switch ($Maker) {
    "HP" {
      Remove-Item -Path "C:\Users\Administrator\Desktop\hp.exe"    
    }
    "Dell Inc." {}
    "Lenovo" {}
  }

  Remove-Item -Recurse -Path "C:\Program Files\Eraser"

  Start-Process -FilePath "C:\Program Files\BurnInTest\unins000.exe" -Wait
  
  Write-Host "Cleanup complete!" -ForegroundColor Green
}

# ==================================================
# Help
# ==================================================
function Help {
  Write-Host @"
Tester - Automatic specification gathering, testing, and configuration tool.

Usage:
  tester.ps1
  tester.ps1 -Help
  tester.ps1 -QROnly
  tester.ps1 -BiosOnly
  tester.ps1 -TestOnly
  tester.ps1 -EraseOnly
"@ -ForegroundColor Green
}

# ==================================================
# User Inputs
# ==================================================
function Get-UserInputs {
    Write-Host "`n--- Condition & Type Entry ---" -ForegroundColor Yellow

    $ChassisMap = @{
	"1" = "Other"
	"2" = "Unknown"
	"3" = "Desktop"
	"4" = "Low Profile Desktop"
	"5" = "Pizza Box"
	"6" = "Mini Tower"
	"7" = "Tower"
	"8" = "Portable"
	"9" = "Laptop"
	"10" = "Notebook"
	"11" = "Handheld"
	"12" = "Docking Station"
	"13" = "All-in-One"
	"14" = "Sub-Notebook"
	"15" = "Space-Saving"
	"16" = "Lunch box"
	"17" = "Main System Chassis"
	"18" = "Expansion Chassis"
	"19" = "SubChassis"
	"20" = "Bus Expansion Chassis"
	"21" = "Peripheral Chassis"
	"22" = "Storage Chassis"
	"23" = "Rack Mount Chassis"
	"24" = "Sealed-Case"
	"30" = "Tablet"
	"31" = "Convertible"
	"32" = "Detachable"
    }

    $ChassisCode = (Get-CimInstance -ClassName Win32_SystemEnclosure).ChassisTypes
    $Screen = $null
    
    # 1. Device Type
    if ($ChassisCode -match "1|2|5|12|15|16|17|18|19|20|21|24") {
	$Chassis = Read-Host "Ambiguous chassis code, please enter PC type (ie. laptop, tower, etc.):"
    } else {
	$Chassis = $ChassisMap["$ChassisCode"]
    }


    if ($ChassisCode -match "8|9|10|11|13|30|31|32" -OR $Chassis -match "laptop|Laptop|All-in-One|AIO") {
	$MonitorParams = Get-CimInstance -Namespace "root\wmi" -ClassName WmiMonitorBasicDisplayParams | Where-Object { $_.InstanceName -eq $InternalMonitor.InstanceName }
        $WidthCM = $MonitorParams.MaxHorizontalImageSize
	$HeightCM = $MonitorParams.MaxVerticalImageSize

	if ($WidthCM -gt 0) {
	    $DiagonalInches = [Math]::Round(([Math]::Sqrt([Math]::Pow($WidthCM, 2) + [Math]::Pow($HeightCM, 2)) / 2.54), 1)
	    $Res = Get-Resolution

	    $Screen = "$($DiagonalInches)in @ $($Res), "
	}
    }

    # 2. Condition Category
    $Cats = @("New - 0%", "Used - 0%", "AS-IS - 40%", "New other (see details) - 0%")
    for ($i=0; $i -lt $Cats.Count; $i++) { Write-Host "[$($i+1)] $($Cats[$i])" }
    $catIdx = Read-Host "Select Condition Category"
    $ConditionCat = $Cats[($catIdx-1)]

    # 3. Description
    $Description = Read-Host "Enter short condition description (e.g. Minor scratches on lid)"

    return @{
	Chassis = $Chassis
	Screen = $Screen
        Cat  = $ConditionCat
        Desc = $Description
    }
}

function Generate-CompressedJson {
    # 1. Gather raw data from your existing functions
    $Inputs = Get-UserInputs # The custom prompts we built
    $Model = (Get-CimInstance Win32_ComputerSystem).Model
    $Brand = (Get-CimInstance Win32_ComputerSystem).Manufacturer.Split(' ')[0] # Gets "Dell" from "Dell Inc."
    $CPU = (Get-CimInstance Win32_Processor).Name.Trim()
    $RAM = Get-RAM
    $OS = (Get-CimInstance Win32_OperatingSystem).Caption -replace "Microsoft ", ""
    $Serial = (Get-CimInstance Win32_Bios).SerialNumber

    if ($Serial -eq "Default string") { $Serial = "" }
    
    # 2. Process Drives & Optical
    $DriveList = Get-Drives
    $DriveStr = ($DriveList | ForEach-Object { "$($_.Size)GB $($_.BusType) $($_.MediaType)" }) -join ", "
    
    $CD = Get-CDDrive
    $OptStr = if ($CD -ne "---") { ", $CD Optical Drive" } else { "" }

    # 3. Process GPUs
    $GPUList = Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name
    if ($GPUList.Count -gt 1) {
        $GPUStr = $GPUList -join " & "
    } else {
        $GPUStr = $GPUList
    }

    # 4. Construct the Master Description String
    # Format: Type (CPU, RAM, Drives, Optical, GPUs, OS)
    $FullDescription = "$($Inputs.Chassis) PC ($($Inputs.Screen)$CPU, $RAM, $DriveStr$OptStr, $GPUStr, $OS)"

    # 5. Build Final JSON Object
    $FinalObj = @{
        "b" = $Brand.ToUpper()         # Brand
        "d" = $FullDescription         # Description
        "c" = $Inputs.Cat              # Condition
        "n" = $Inputs.Desc             # Notes
        "s" = $Model                   # Style
        "e" = $Serial                  # Serial
    }

    Write-Host $($FinalObj | ConvertTo-Json)

	$Json = $FinalObj | ConvertTo-Json -Compress
	$Bytes = [System.Text.Encoding]::UTF8.GetBytes($Json)
	return [Convert]::ToBase64String($Bytes)
}

function Show-QR-Local {
    $Data = Generate-CompressedJson

    $OutPath = "$env:TEMP\specs_qr.png"
    # $PSScriptRoot points to the folder where the script is running (your USB)
    $ExePath = Join-Path $PSScriptRoot "qrencode.exe"

    if (Test-Path $ExePath) {
        # -s 10: Pixel size (makes it large/readable)
        # -m 2: Margin size
        # -l H: High error correction (good for scanning off screens)
        & $ExePath -o $OutPath -s 10 -m 2 -l H "$Data"
        
	winget install photos
        Start-Process $OutPath
    } else {
        Write-Host "Local QR Tool not found at $ExePath" -ForegroundColor Red
    }
}

# ==================================================
# Entry point
# ==================================================
Start-Transcript -Path $LogFile -Append

if ($Help) { Help }
elseif ($QROnly) { Show-QR-Local }
elseif ($BiosOnly) { Handle-BIOS }
elseif ($TestOnly) { Start-BurnInTest }
elseif ($EraseOnly) { Start-Eraser }
elseif ($Deprecated) { Get-Specs; Handle-BIOS; Start-BurnInTest; Start-Eraser; Cleanup }
else { Handle-GPU; Handle-BIOS; Start-BurnInTest; Start-Eraser; Cleanup; Show-QR-Local }

Stop-Transcript
