###############################################################################
#### windows-backup-smoke-tests.ps1 — Verify Windows backup health         ####
#### Checks WBAdmin status, backup age, VSS writers, shadow copies,        ####
#### system state, event log errors, SQL backup age, BMR readiness.        ####
####                                                                       ####
#### Author:  Phil Connor                                                  ####
#### Contact: contact@mylinux.work                                         ####
#### License: MIT                                                          ####
#### Version: 1.0                                                          ####
####                                                                       ####
#### Usage:   .\windows-backup-smoke-tests.ps1                             ####
####          .\windows-backup-smoke-tests.ps1 -BackupTarget E:\Backups    ####
####          .\windows-backup-smoke-tests.ps1 -OutputFormat tap           ####
####                                                                       ####
#### See -Help for all options.                                            ####
###############################################################################

[CmdletBinding()]
param(
    [string]$BackupTarget = "",
    [int]$MaxBackupAgeHours = 25,
    [ValidateSet("text","tap")]
    [string]$OutputFormat = "text",
    [switch]$NoColor,
    [switch]$Help
)

$ErrorActionPreference = "Continue"

# ============================================================================
# HELP
# ============================================================================

if ($Help) {
    @"
Usage: .\windows-backup-smoke-tests.ps1 [OPTIONS]

Smoke-test Windows backup infrastructure. PowerShell 5.1+.
Designed for Windows Server with Windows Server Backup.

Parameters:
  -BackupTarget PATH        Backup destination path (default: auto-detect from WBAdmin)
  -MaxBackupAgeHours HOURS  Maximum backup age in hours (default: 25)
  -OutputFormat FORMAT      Output: text (default), tap
  -NoColor                  Disable coloured output
  -Verbose                  Show debug output
  -Help                     Show this help

Examples:
  .\windows-backup-smoke-tests.ps1
  .\windows-backup-smoke-tests.ps1 -BackupTarget "E:\WindowsImageBackup"
  .\windows-backup-smoke-tests.ps1 -MaxBackupAgeHours 48 -OutputFormat tap
  .\windows-backup-smoke-tests.ps1 -NoColor -Verbose
"@
    exit 0
}

# ============================================================================
# STATE
# ============================================================================

$script:Pass = 0
$script:Fail = 0
$script:Skip = 0
$script:Total = 0
$script:Results = @()
$script:StartTime = Get-Date

# ============================================================================
# COLORS
# ============================================================================

function Write-Color {
    param([string]$Text, [string]$Color = "White")
    if ($NoColor) {
        Write-Host $Text
    } else {
        Write-Host $Text -ForegroundColor $Color
    }
}

function Write-Log   { param([string]$Msg) Write-Color "[INFO] $Msg" "Cyan" }
function Write-Warn  { param([string]$Msg) Write-Color "[WARN] $Msg" "Yellow" }
function Write-Err   { param([string]$Msg) Write-Color "[ERROR] $Msg" "Red" }

# ============================================================================
# TEST RESULT RECORDING
# ============================================================================

function Record-Pass {
    param([string]$Name, [string]$Detail = "")
    $script:Pass++
    $script:Total++
    $script:Results += [PSCustomObject]@{ Status="PASS"; Name=$Name; Detail=$Detail }
    if ($OutputFormat -eq "tap") {
        Write-Host "ok $($script:Total) - $Name$(if($Detail){" ($Detail)"})"
    } else {
        $mark = if ($NoColor) { "[PASS]" } else { [char]0x2713 }
        $msg = "  $mark $Name"
        if ($Detail) { $msg += " - $Detail" }
        Write-Color $msg "Green"
    }
}

function Record-Fail {
    param([string]$Name, [string]$Detail = "")
    $script:Fail++
    $script:Total++
    $script:Results += [PSCustomObject]@{ Status="FAIL"; Name=$Name; Detail=$Detail }
    if ($OutputFormat -eq "tap") {
        Write-Host "not ok $($script:Total) - $Name"
        if ($Detail) { Write-Host "  # $Detail" }
    } else {
        $mark = if ($NoColor) { "[FAIL]" } else { [char]0x2717 }
        $msg = "  $mark $Name"
        if ($Detail) { $msg += " - $Detail" }
        Write-Color $msg "Red"
    }
}

function Record-Skip {
    param([string]$Name, [string]$Reason = "")
    $script:Skip++
    $script:Total++
    $script:Results += [PSCustomObject]@{ Status="SKIP"; Name=$Name; Detail=$Reason }
    if ($OutputFormat -eq "tap") {
        Write-Host "ok $($script:Total) - $Name # SKIP $Reason"
    } else {
        $mark = if ($NoColor) { "[SKIP]" } else { [char]0x2298 }
        $msg = "  $mark $Name"
        if ($Reason) { $msg += " - $Reason" }
        Write-Color $msg "Yellow"
    }
}

# ============================================================================
# HELPERS
# ============================================================================

function Test-CommandExists {
    param([string]$Command)
    $null -ne (Get-Command $Command -ErrorAction SilentlyContinue)
}

function Write-Section {
    param([string]$Name)
    if ($OutputFormat -eq "text") {
        Write-Host ""
        Write-Color $Name "White"
    }
}

# ============================================================================
# TESTS
# ============================================================================

# -- 1. Windows Server Backup Feature ---------------------------------------

function Test-BackupFeature {
    Write-Section "Backup Feature"

    if (Test-CommandExists "Get-WindowsFeature") {
        try {
            $feature = Get-WindowsFeature -Name Windows-Server-Backup -ErrorAction Stop
            if ($feature.Installed) {
                Record-Pass "Windows Server Backup feature installed"
            } else {
                Record-Fail "Windows Server Backup feature installed" "feature not installed"
            }
        } catch {
            Record-Fail "Windows Server Backup feature installed" $_.Exception.Message
        }
    } else {
        # Workstation or older OS — check for wbadmin directly
        if (Test-CommandExists "wbadmin") {
            Record-Pass "Windows Server Backup feature installed" "wbadmin available"
        } else {
            Record-Skip "Windows Server Backup feature installed" "Get-WindowsFeature not available"
        }
    }
}

# -- 2. Backup Scheduled Task -----------------------------------------------

function Test-BackupScheduledTask {
    Write-Section "Scheduled Task"

    try {
        $task = Get-ScheduledTask -TaskName "Microsoft-Windows-WindowsBackup" -TaskPath "\Microsoft\Windows\WindowsBackup\" -ErrorAction SilentlyContinue
        if (-not $task) {
            # Try alternative task name
            $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object {
                $_.TaskPath -match "WindowsBackup" -or $_.TaskName -match "WindowsBackup"
            } | Select-Object -First 1
        }

        if ($task) {
            if ($task.State -eq "Ready" -or $task.State -eq "Running") {
                Record-Pass "Backup scheduled task enabled" "state: $($task.State)"
            } else {
                Record-Fail "Backup scheduled task enabled" "state: $($task.State)"
            }
        } else {
            Record-Fail "Backup scheduled task enabled" "no backup scheduled task found"
        }
    } catch {
        Record-Fail "Backup scheduled task enabled" $_.Exception.Message
    }
}

# -- 3. Last Backup Status --------------------------------------------------

function Test-LastBackupStatus {
    Write-Section "Backup Status"

    if (-not (Test-CommandExists "wbadmin")) {
        Record-Skip "Last backup status" "wbadmin not available"
        return
    }

    try {
        $output = wbadmin get versions 2>&1 | Out-String
        if ($output -match "Backup time:\s*(.+)" -or $output -match "Version identifier:\s*(.+)") {
            Record-Pass "Last backup status" "completed successfully"
        } elseif ($output -match "no previous backups" -or $output -match "There are no versions") {
            Record-Fail "Last backup status" "no backups found"
        } else {
            Record-Fail "Last backup status" "could not parse wbadmin output"
        }
    } catch {
        Record-Fail "Last backup status" "wbadmin error - $($_.Exception.Message)"
    }
}

# -- 4. Backup Age ----------------------------------------------------------

function Test-BackupAge {
    if (-not (Test-CommandExists "wbadmin")) {
        Record-Skip "Backup age" "wbadmin not available"
        return
    }

    try {
        $output = wbadmin get versions 2>&1 | Out-String
        $timestamps = [regex]::Matches($output, "Backup time:\s*(.+)")

        if ($timestamps.Count -eq 0) {
            Record-Fail "Backup age" "no backup timestamps found"
            return
        }

        # Get the most recent backup time
        $latestStr = $timestamps[$timestamps.Count - 1].Groups[1].Value.Trim()
        $latestTime = [datetime]::Parse($latestStr)
        $ageHours = [math]::Floor(((Get-Date) - $latestTime).TotalHours)

        if ($ageHours -le $MaxBackupAgeHours) {
            Record-Pass "Backup age" "${ageHours}h < ${MaxBackupAgeHours}h threshold"
        } else {
            Record-Fail "Backup age" "${ageHours}h exceeds ${MaxBackupAgeHours}h threshold"
        }
    } catch {
        Record-Fail "Backup age" "could not determine backup age - $($_.Exception.Message)"
    }
}

# -- 5. Backup Target Accessible --------------------------------------------

function Test-BackupTargetAccessible {
    Write-Section "Backup Target"

    $target = $BackupTarget

    # Auto-detect if not specified
    if (-not $target -and (Test-CommandExists "wbadmin")) {
        try {
            $policyOutput = wbadmin get policy 2>&1 | Out-String
            if ($policyOutput -match "Backup Target:\s*(.+)") {
                $target = $Matches[1].Trim()
                Write-Verbose "Auto-detected backup target: $target"
            }
        } catch {
            Write-Verbose "Could not auto-detect backup target from policy"
        }
    }

    if (-not $target) {
        Record-Skip "Backup target accessible" "no backup target specified or detected"
        return
    }

    try {
        if (Test-Path $target -ErrorAction Stop) {
            Record-Pass "Backup target accessible" $target
        } else {
            Record-Fail "Backup target accessible" "$target - not accessible"
        }
    } catch {
        Record-Fail "Backup target accessible" "$target - $($_.Exception.Message)"
    }
}

# -- 6. VSS Writers Healthy -------------------------------------------------

function Test-VSSWriters {
    Write-Section "VSS"

    if (-not (Test-CommandExists "vssadmin")) {
        Record-Skip "VSS writers healthy" "vssadmin not available"
        return
    }

    try {
        $output = vssadmin list writers 2>&1 | Out-String
        $writerBlocks = [regex]::Matches($output, "Writer name:\s*'([^']+)'.+?State:\s*\[(\d+)\]\s*(\w+)", [System.Text.RegularExpressions.RegexOptions]::Singleline)

        if ($writerBlocks.Count -eq 0) {
            Record-Fail "VSS writers healthy" "could not parse writer state"
            return
        }

        $failedWriters = @()
        foreach ($match in $writerBlocks) {
            $writerName = $match.Groups[1].Value
            $stateNum = $match.Groups[2].Value
            $stateText = $match.Groups[3].Value
            if ($stateText -ne "Stable" -and $stateText -ne "Waiting") {
                $failedWriters += "$writerName ($stateText)"
            }
        }

        if ($failedWriters.Count -eq 0) {
            Record-Pass "VSS writers healthy" "$($writerBlocks.Count) writers, 0 failed"
        } else {
            Record-Fail "VSS writers healthy" "$($failedWriters.Count) failed: $($failedWriters -join ', ')"
        }
    } catch {
        Record-Fail "VSS writers healthy" "vssadmin error - $($_.Exception.Message)"
    }
}

# -- 7. VSS Shadow Copies Exist ---------------------------------------------

function Test-VSSShadowCopies {
    if (-not (Test-CommandExists "vssadmin")) {
        Record-Skip "VSS shadow copies exist" "vssadmin not available"
        return
    }

    try {
        $output = vssadmin list shadows 2>&1 | Out-String
        $shadowCount = ([regex]::Matches($output, "Shadow Copy ID:")).Count

        if ($shadowCount -gt 0) {
            Record-Pass "VSS shadow copies exist" "$shadowCount shadow copies"
        } elseif ($output -match "No items found") {
            Record-Fail "VSS shadow copies exist" "no shadow copies found"
        } else {
            Record-Fail "VSS shadow copies exist" "no shadow copies detected"
        }
    } catch {
        Record-Fail "VSS shadow copies exist" "vssadmin error - $($_.Exception.Message)"
    }
}

# -- 8. System State Backup -------------------------------------------------

function Test-SystemStateBackup {
    Write-Section "System State"

    if (-not (Test-CommandExists "wbadmin")) {
        Record-Skip "System state backup present" "wbadmin not available"
        return
    }

    try {
        $output = wbadmin get versions 2>&1 | Out-String
        if ($output -match "Can recover:\s*.*System State" -or $output -match "systemStateBackup" -or $output -match "System State") {
            Record-Pass "System state backup present"
        } elseif ($output -match "no previous backups" -or $output -match "There are no versions") {
            Record-Fail "System state backup present" "no backups found"
        } else {
            Record-Fail "System state backup present" "system state not included in backups"
        }
    } catch {
        Record-Fail "System state backup present" "wbadmin error - $($_.Exception.Message)"
    }
}

# -- 9. Backup Disk Space ---------------------------------------------------

function Test-BackupDiskSpace {
    Write-Section "Disk Space"

    $target = $BackupTarget
    if (-not $target -and (Test-CommandExists "wbadmin")) {
        try {
            $policyOutput = wbadmin get policy 2>&1 | Out-String
            if ($policyOutput -match "Backup Target:\s*(.+)") { $target = $Matches[1].Trim() }
        } catch {}
    }

    if (-not $target) { Record-Skip "Backup disk space" "no backup target specified or detected"; return }

    try {
        if ($target -match "^([A-Za-z]):\\") {
            $drive = Get-PSDrive -Name $Matches[1] -ErrorAction Stop
            $freeGB = [math]::Round($drive.Free / 1GB, 0)
            if ($freeGB -ge 10) { Record-Pass "Backup disk space" "$freeGB GB free on $($Matches[1]):" }
            elseif ($freeGB -ge 1) { Record-Pass "Backup disk space" "$freeGB GB free on $($Matches[1]): (low)" }
            else { Record-Fail "Backup disk space" "$freeGB GB free on $($Matches[1]): — critically low" }
        } elseif ($target -match "^\\\\") {
            if (Test-Path $target -ErrorAction Stop) { Record-Pass "Backup disk space" "UNC target accessible (space check not available)" }
            else { Record-Fail "Backup disk space" "UNC target not accessible" }
        } else {
            Record-Skip "Backup disk space" "cannot determine volume from target: $target"
        }
    } catch {
        Record-Fail "Backup disk space" $_.Exception.Message
    }
}

# -- 10. Backup Event Log ---------------------------------------------------

function Test-BackupEventLog {
    Write-Section "Event Log"

    try {
        $logName = "Microsoft-Windows-Backup"
        $cutoff = (Get-Date).AddHours(-24)

        $errors = Get-WinEvent -FilterHashtable @{
            LogName     = "$logName/Operational"
            Level       = 2  # Error
            StartTime   = $cutoff
        } -ErrorAction SilentlyContinue

        if (-not $errors) {
            # Try alternative log name
            $errors = Get-WinEvent -FilterHashtable @{
                ProviderName = "Microsoft-Windows-Backup"
                Level        = 2
                StartTime    = $cutoff
            } -ErrorAction SilentlyContinue
        }

        $errorCount = if ($errors) { ($errors | Measure-Object).Count } else { 0 }

        if ($errorCount -eq 0) {
            Record-Pass "Backup event log clean" "0 errors in 24h"
        } else {
            $latestMsg = $errors[0].Message
            if ($latestMsg.Length -gt 80) { $latestMsg = $latestMsg.Substring(0, 80) + "..." }
            Record-Fail "Backup event log clean" "$errorCount error(s) in 24h — latest: $latestMsg"
        }
    } catch {
        if ($_.Exception.Message -match "No events were found" -or $_.Exception.Message -match "could not be found") {
            Record-Pass "Backup event log clean" "0 errors in 24h"
        } else {
            Record-Skip "Backup event log clean" "cannot access backup event log"
        }
    }
}

# -- 11. SQL Server Backup Age ----------------------------------------------

function Test-SQLBackupAge {
    Write-Section "SQL Server"

    # Check if SQL Server is installed
    $sqlService = Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue
    if (-not $sqlService) {
        $sqlService = Get-Service -ErrorAction SilentlyContinue | Where-Object {
            $_.Name -match "^MSSQL\$" -or $_.Name -eq "MSSQLSERVER"
        } | Select-Object -First 1
    }

    if (-not $sqlService) {
        Record-Skip "SQL Server backup age" "SQL Server not installed"
        return
    }

    $query = @"
SELECT TOP 1
    bs.database_name,
    bs.backup_finish_date,
    DATEDIFF(HOUR, bs.backup_finish_date, GETDATE()) AS age_hours
FROM msdb.dbo.backupset bs
ORDER BY bs.backup_finish_date DESC
"@

    # Try Invoke-Sqlcmd, then fall back to sqlcmd
    $ageH = $null
    $dbName = ""

    if (Test-CommandExists "Invoke-Sqlcmd") {
        try {
            $result = Invoke-Sqlcmd -Query $query -ServerInstance "localhost" -ErrorAction Stop
            if ($result) { $ageH = $result.age_hours; $dbName = $result.database_name }
            else { Record-Fail "SQL Server backup age" "no SQL backups found in msdb"; return }
        } catch { Write-Verbose "Invoke-Sqlcmd failed: $($_.Exception.Message)" }
    }

    if ($null -eq $ageH -and (Test-CommandExists "sqlcmd")) {
        try {
            $output = sqlcmd -S localhost -Q $query -h -1 -W 2>&1 | Out-String
            if ($output -match "(\d+)\s*$") { $ageH = [int]$Matches[1] }
            else { Record-Fail "SQL Server backup age" "could not parse sqlcmd output"; return }
        } catch { Write-Verbose "sqlcmd failed: $($_.Exception.Message)" }
    }

    if ($null -ne $ageH) {
        $detail = "${ageH}h$(if($dbName){" ($dbName)"})"
        if ($ageH -le $MaxBackupAgeHours) { Record-Pass "SQL Server backup age" $detail }
        else { Record-Fail "SQL Server backup age" "$detail exceeds ${MaxBackupAgeHours}h threshold" }
    } else {
        Record-Skip "SQL Server backup age" "SQL Server present but query tools unavailable"
    }
}

# -- 12. Backup Schedule Configured ------------------------------------------

function Test-BackupSchedule {
    Write-Section "Schedule"

    if (-not (Test-CommandExists "wbadmin")) {
        Record-Skip "Backup schedule configured" "wbadmin not available"
        return
    }

    try {
        $output = wbadmin get policy 2>&1 | Out-String

        if ($output -match "Schedule:" -or $output -match "Times of day:") {
            Record-Pass "Backup schedule configured"
        } elseif ($output -match "no current backup policy" -or $output -match "There is no currently-set") {
            Record-Fail "Backup schedule configured" "no backup policy configured"
        } else {
            Record-Fail "Backup schedule configured" "could not confirm schedule in policy output"
        }
    } catch {
        Record-Fail "Backup schedule configured" "wbadmin error - $($_.Exception.Message)"
    }
}

# -- 13. Bare Metal Recovery ------------------------------------------------

function Test-BareMetalRecovery {
    Write-Section "Bare Metal Recovery"

    if (-not (Test-CommandExists "wbadmin")) {
        Record-Skip "Bare metal recovery components included" "wbadmin not available"
        return
    }

    try {
        # Check policy for BMR
        $policyOutput = wbadmin get policy 2>&1 | Out-String
        $hasBMRPolicy = $policyOutput -match "bare metal recovery" -or $policyOutput -match "baremetal" -or $policyOutput -match "BMR"

        # Check versions for BMR
        $versionsOutput = wbadmin get versions 2>&1 | Out-String
        $hasBMRVersion = $versionsOutput -match "Bare Metal Recovery" -or $versionsOutput -match "Can recover:.*Full Server"

        if ($hasBMRPolicy -or $hasBMRVersion) {
            Record-Pass "Bare metal recovery components included"
        } elseif ($versionsOutput -match "no previous backups" -or $versionsOutput -match "There are no versions") {
            Record-Fail "Bare metal recovery components included" "no backups found to check"
        } else {
            Record-Fail "Bare metal recovery components included" "BMR not detected in backup policy or versions"
        }
    } catch {
        Record-Fail "Bare metal recovery components included" "wbadmin error - $($_.Exception.Message)"
    }
}

# ============================================================================
# OUTPUT
# ============================================================================

function Write-Header {
    if ($OutputFormat -eq "tap") {
        Write-Host "TAP version 13"
    } else {
        Write-Host ""
        Write-Color "Windows Backup Smoke Tests" "White"
        Write-Host "Host:   $($env:COMPUTERNAME)"
        Write-Host "Target: $(if ($BackupTarget) { $BackupTarget } else { '(auto-detect)' })"
        Write-Host "Time:   $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
    }
}

function Write-Summary {
    $duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)

    if ($OutputFormat -eq "tap") {
        Write-Host "1..$($script:Total)"
        Write-Host "# pass $($script:Pass)"
        Write-Host "# fail $($script:Fail)"
        Write-Host "# skip $($script:Skip)"
    } else {
        Write-Host ""
        $separator = [string]::new([char]0x2500, 40)
        Write-Color $separator "White"
        Write-Color "Summary  $($env:COMPUTERNAME)" "White"

        $summaryLine = "  $($script:Pass) passed  $($script:Fail) failed  $($script:Skip) skipped  (${duration}s)"
        Write-Host $summaryLine
        Write-Color $separator "White"

        if ($script:Fail -eq 0) {
            Write-Color "All tests passed." "Green"
        } else {
            Write-Color "$($script:Fail) test(s) failed." "Red"
        }
    }
}

# ============================================================================
# MAIN
# ============================================================================

Write-Header

# Run all tests
Test-BackupFeature
Test-BackupScheduledTask
Test-LastBackupStatus
Test-BackupAge
Test-BackupTargetAccessible
Test-VSSWriters
Test-VSSShadowCopies
Test-SystemStateBackup
Test-BackupDiskSpace
Test-BackupEventLog
Test-SQLBackupAge
Test-BackupSchedule
Test-BareMetalRecovery

Write-Summary

if ($script:Fail -eq 0) { exit 0 } else { exit 1 }
