###############################################################################
#### windows-ad-smoke-tests.ps1 — Verify Active Directory health           ####
#### Checks DC connectivity, LDAP, DNS, replication, FSMO, shares,         ####
#### Kerberos, Group Policy, ADWS, trusts, and time sync.                  ####
####                                                                       ####
#### Author:  Phil Connor                                                  ####
#### Contact: contact@mylinux.work                                         ####
#### License: MIT                                                          ####
#### Version: 1.0                                                          ####
####                                                                       ####
#### Usage:   .\windows-ad-smoke-tests.ps1                                 ####
####          .\windows-ad-smoke-tests.ps1 -DomainController DC01          ####
####          .\windows-ad-smoke-tests.ps1 -OutputFormat tap               ####
####                                                                       ####
#### See -Help for all options.                                            ####
###############################################################################

[CmdletBinding()]
param(
    [string]$DomainController = "",
    [string]$Domain = "",
    [ValidateSet("text","tap")]
    [string]$OutputFormat = "text",
    [switch]$NoColor,
    [switch]$Help
)

$ErrorActionPreference = "Continue"

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

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

Smoke-test Active Directory infrastructure. PowerShell 5.1+.
Designed for domain-joined Windows machines.

Parameters:
  -DomainController DC   Target DC hostname or IP (default: auto-detect via nltest)
  -Domain FQDN           AD domain FQDN (default: auto-detect via USERDNSDOMAIN)
  -OutputFormat FORMAT    Output: text (default), tap
  -NoColor               Disable coloured output
  -Verbose               Show debug output
  -Help                  Show this help

Examples:
  .\windows-ad-smoke-tests.ps1
  .\windows-ad-smoke-tests.ps1 -DomainController DC01.corp.local
  .\windows-ad-smoke-tests.ps1 -Domain corp.local -OutputFormat tap
  .\windows-ad-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"
    }
}

# ============================================================================
# AUTO-DETECTION
# ============================================================================

function Resolve-DomainController {
    if ($DomainController) {
        Write-Verbose "Using specified DC: $DomainController"
        return $DomainController
    }

    try {
        $nltest = nltest /dsgetdc: 2>&1
        $dcLine = $nltest | Where-Object { $_ -match "DC: \\\\" }
        if ($dcLine -match "DC: \\\\(.+)$") {
            $detected = $Matches[1].Trim()
            Write-Verbose "Auto-detected DC: $detected"
            return $detected
        }
    } catch {}

    try {
        $dcLocator = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
        $dc = $dcLocator.FindDomainController()
        if ($dc.Name) {
            Write-Verbose "Auto-detected DC via .NET: $($dc.Name)"
            return $dc.Name
        }
    } catch {}

    Write-Err "Cannot auto-detect domain controller. Use -DomainController parameter."
    exit 1
}

function Resolve-Domain {
    if ($Domain) {
        Write-Verbose "Using specified domain: $Domain"
        return $Domain
    }

    if ($env:USERDNSDOMAIN) {
        Write-Verbose "Auto-detected domain: $($env:USERDNSDOMAIN)"
        return $env:USERDNSDOMAIN
    }

    try {
        $dom = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
        if ($dom.Name) {
            Write-Verbose "Auto-detected domain via .NET: $($dom.Name)"
            return $dom.Name
        }
    } catch {}

    Write-Err "Cannot auto-detect domain. Use -Domain parameter."
    exit 1
}

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

# -- 1. DC Connectivity ----------------------------------------------------

function Test-DCConnectivity {
    Write-Section "Connectivity"

    try {
        $ping = Test-Connection -ComputerName $script:DC -Count 2 -Quiet -ErrorAction Stop
        if ($ping) {
            Record-Pass "DC connectivity" $script:DC
        } else {
            Record-Fail "DC connectivity" "$($script:DC) - no response"
        }
    } catch {
        Record-Fail "DC connectivity" "$($script:DC) - $($_.Exception.Message)"
    }
}

# -- 2. LDAP Bind -----------------------------------------------------------

function Test-LDAPBind {
    Write-Section "LDAP"

    try {
        $ldapPath = "LDAP://$($script:DC)"
        $entry = New-Object System.DirectoryServices.DirectoryEntry($ldapPath)
        $searcher = New-Object System.DirectoryServices.DirectorySearcher($entry)
        $searcher.SearchScope = "Base"
        $searcher.PropertiesToLoad.Add("defaultNamingContext") | Out-Null
        $result = $searcher.FindOne()

        if ($result) {
            $nc = $result.Properties["defaultnamingcontext"][0]
            Record-Pass "LDAP bind" "$($script:DC) - $nc"
        } else {
            Record-Fail "LDAP bind" "$($script:DC) - bind succeeded but no result"
        }

        $searcher.Dispose()
        $entry.Dispose()
    } catch {
        Record-Fail "LDAP bind" "$($script:DC) - $($_.Exception.Message)"
    }
}

# -- 3. LDAPS Connectivity --------------------------------------------------

function Test-LDAPS {
    try {
        $tcp = Test-NetConnection -ComputerName $script:DC -Port 636 -WarningAction SilentlyContinue -ErrorAction Stop
        if ($tcp.TcpTestSucceeded) {
            Record-Pass "LDAPS connectivity" "$($script:DC):636"
        } else {
            Record-Fail "LDAPS connectivity" "$($script:DC):636 - port closed"
        }
    } catch {
        if (Test-CommandExists "Test-NetConnection") {
            Record-Fail "LDAPS connectivity" "$($script:DC):636 - $($_.Exception.Message)"
        } else {
            Record-Skip "LDAPS connectivity" "Test-NetConnection not available"
        }
    }
}

# -- 4. DNS SRV Records ----------------------------------------------------

function Test-DNSSrvRecords {
    Write-Section "DNS"

    $srvRecord = "_ldap._tcp.dc._msdcs.$($script:DomainFQDN)"

    try {
        if (Test-CommandExists "Resolve-DnsName") {
            $results = Resolve-DnsName -Name $srvRecord -Type SRV -ErrorAction Stop
            if ($results) {
                $count = ($results | Where-Object { $_.QueryType -eq "SRV" }).Count
                Record-Pass "DNS SRV records" "$srvRecord - $count record(s)"
            } else {
                Record-Fail "DNS SRV records" "$srvRecord - no records returned"
            }
        } else {
            $nslookup = nslookup -type=srv $srvRecord 2>&1
            if ($nslookup -match "svr hostname") {
                Record-Pass "DNS SRV records" $srvRecord
            } else {
                Record-Fail "DNS SRV records" "$srvRecord - lookup failed"
            }
        }
    } catch {
        Record-Fail "DNS SRV records" "$srvRecord - $($_.Exception.Message)"
    }
}

# -- 5. AD Replication ------------------------------------------------------

function Test-ADReplication {
    Write-Section "Replication"

    $adModuleLoaded = Get-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
    if (-not $adModuleLoaded) {
        try { Import-Module ActiveDirectory -ErrorAction Stop } catch {}
        $adModuleLoaded = Get-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
    }

    if ($adModuleLoaded) {
        try {
            $partners = Get-ADReplicationPartnerMetadata -Target $script:DC -ErrorAction Stop
            $failures = $partners | Where-Object { $_.ConsecutiveReplicationFailures -gt 0 }
            if ($failures) {
                $failCount = ($failures | Measure-Object).Count
                Record-Fail "AD replication status" "$failCount partner(s) with failures"
            } else {
                $partnerCount = ($partners | Measure-Object).Count
                Record-Pass "AD replication status" "$partnerCount partner(s), no failures"
            }
            return
        } catch {
            Write-Verbose "Get-ADReplicationPartnerMetadata failed: $($_.Exception.Message)"
        }
    }

    if (Test-CommandExists "repadmin") {
        try {
            $output = repadmin /replsummary $script:DC 2>&1 | Out-String
            if ($output -match "failed") {
                Record-Fail "AD replication status" "repadmin reports failures"
            } elseif ($output -match "Source DSA") {
                Record-Pass "AD replication status" "repadmin reports no failures"
            } else {
                Record-Pass "AD replication status" "repadmin completed"
            }
        } catch {
            Record-Fail "AD replication status" "repadmin error - $($_.Exception.Message)"
        }
    } else {
        Record-Skip "AD replication status" "neither AD module nor repadmin available"
    }
}

# -- 6. FSMO Roles ----------------------------------------------------------

function Test-FSMORoles {
    Write-Section "FSMO"

    if (Test-CommandExists "netdom") {
        try {
            $output = netdom query fsmo 2>&1 | Out-String
            $roles = @(
                "Schema master",
                "Domain naming master",
                "PDC",
                "RID pool manager",
                "Infrastructure master"
            )
            $found = 0
            foreach ($role in $roles) {
                if ($output -match "$role\s+\S+") {
                    $found++
                }
            }
            if ($found -eq 5) {
                Record-Pass "FSMO roles assigned" "all 5 roles located"
            } elseif ($found -gt 0) {
                Record-Fail "FSMO roles assigned" "only $found of 5 roles found"
            } else {
                Record-Fail "FSMO roles assigned" "no roles found in netdom output"
            }
        } catch {
            Record-Fail "FSMO roles assigned" "netdom error - $($_.Exception.Message)"
        }
    } else {
        $adModuleLoaded = Get-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
        if ($adModuleLoaded) {
            try {
                $forest = Get-ADForest -ErrorAction Stop
                $domain = Get-ADDomain -ErrorAction Stop
                $roleCount = 0
                if ($forest.SchemaMaster)        { $roleCount++ }
                if ($forest.DomainNamingMaster)   { $roleCount++ }
                if ($domain.PDCEmulator)          { $roleCount++ }
                if ($domain.RIDMaster)            { $roleCount++ }
                if ($domain.InfrastructureMaster) { $roleCount++ }

                if ($roleCount -eq 5) {
                    Record-Pass "FSMO roles assigned" "all 5 roles located via AD module"
                } else {
                    Record-Fail "FSMO roles assigned" "only $roleCount of 5 roles found"
                }
            } catch {
                Record-Fail "FSMO roles assigned" "AD module error - $($_.Exception.Message)"
            }
        } else {
            Record-Skip "FSMO roles assigned" "neither netdom nor AD module available"
        }
    }
}

# -- 7. SYSVOL Share --------------------------------------------------------

function Test-SYSVOLShare {
    Write-Section "Shares"

    $sysvolPath = "\\$($script:DC)\SYSVOL"
    try {
        if (Test-Path $sysvolPath -ErrorAction Stop) {
            Record-Pass "SYSVOL share accessible" $sysvolPath
        } else {
            Record-Fail "SYSVOL share accessible" "$sysvolPath - not accessible"
        }
    } catch {
        Record-Fail "SYSVOL share accessible" "$sysvolPath - $($_.Exception.Message)"
    }
}

# -- 8. NETLOGON Share ------------------------------------------------------

function Test-NETLOGONShare {
    $netlogonPath = "\\$($script:DC)\NETLOGON"
    try {
        if (Test-Path $netlogonPath -ErrorAction Stop) {
            Record-Pass "NETLOGON share accessible" $netlogonPath
        } else {
            Record-Fail "NETLOGON share accessible" "$netlogonPath - not accessible"
        }
    } catch {
        Record-Fail "NETLOGON share accessible" "$netlogonPath - $($_.Exception.Message)"
    }
}

# -- 9. Kerberos -----------------------------------------------------------

function Test-Kerberos {
    Write-Section "Authentication"

    try {
        $secure = Test-ComputerSecureChannel -ErrorAction Stop
        if ($secure) {
            Record-Pass "Kerberos secure channel" "secure channel OK"
        } else {
            Record-Fail "Kerberos secure channel" "secure channel broken"
        }
    } catch {
        if (Test-CommandExists "klist") {
            try {
                $klist = klist 2>&1 | Out-String
                if ($klist -match "krbtgt") {
                    Record-Pass "Kerberos secure channel" "TGT present (klist)"
                } else {
                    Record-Fail "Kerberos secure channel" "no TGT found (klist)"
                }
            } catch {
                Record-Fail "Kerberos secure channel" "klist error - $($_.Exception.Message)"
            }
        } else {
            Record-Fail "Kerberos secure channel" $_.Exception.Message
        }
    }
}

# -- 10. Group Policy -------------------------------------------------------

function Test-GroupPolicy {
    if (Test-CommandExists "gpresult") {
        try {
            $output = gpresult /r 2>&1 | Out-String
            if ($output -match "Applied Group Policy Objects" -or $output -match "Last time Group Policy was applied") {
                $lastApplied = ""
                if ($output -match "Last time Group Policy was applied:\s*(.+)") {
                    $lastApplied = $Matches[1].Trim()
                }
                Record-Pass "Group Policy processing" $(if ($lastApplied) { "last applied: $lastApplied" } else { "policy applied" })
            } elseif ($output -match "ERROR\b|Access is denied") {
                Record-Fail "Group Policy processing" "gpresult returned an error"
            } else {
                Record-Fail "Group Policy processing" "could not confirm policy application"
            }
        } catch {
            Record-Fail "Group Policy processing" "gpresult error - $($_.Exception.Message)"
        }
    } else {
        Record-Skip "Group Policy processing" "gpresult not available"
    }
}

# -- 11. AD Web Services (ADWS) --------------------------------------------

function Test-ADWS {
    Write-Section "Services"

    try {
        $tcp = Test-NetConnection -ComputerName $script:DC -Port 9389 -WarningAction SilentlyContinue -ErrorAction Stop
        if ($tcp.TcpTestSucceeded) {
            Record-Pass "AD Web Services" "$($script:DC):9389"
        } else {
            Record-Fail "AD Web Services" "$($script:DC):9389 - port closed"
        }
    } catch {
        if (Test-CommandExists "Test-NetConnection") {
            Record-Fail "AD Web Services" "$($script:DC):9389 - $($_.Exception.Message)"
        } else {
            Record-Skip "AD Web Services" "Test-NetConnection not available"
        }
    }
}

# -- 12. Domain Trusts ------------------------------------------------------

function Test-DomainTrusts {
    Write-Section "Trusts"

    if (Test-CommandExists "nltest") {
        try {
            $output = nltest /trusted_domains 2>&1 | Out-String
            if ($output -match "The command completed successfully") {
                $trustLines = ($output -split "`n") | Where-Object {
                    $_ -match "\S" -and
                    $_ -notmatch "The command completed" -and
                    $_ -notmatch "^$"
                }
                $trustCount = ($trustLines | Measure-Object).Count
                Record-Pass "Domain trusts" "$trustCount trust(s) enumerated"
            } elseif ($output -match "ERROR_NO_SUCH_DOMAIN") {
                Record-Fail "Domain trusts" "domain not found"
            } else {
                Record-Fail "Domain trusts" "nltest failed"
            }
        } catch {
            Record-Fail "Domain trusts" "nltest error - $($_.Exception.Message)"
        }
    } else {
        $adModuleLoaded = Get-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
        if ($adModuleLoaded) {
            try {
                $trusts = Get-ADTrust -Filter * -ErrorAction Stop
                $trustCount = ($trusts | Measure-Object).Count
                Record-Pass "Domain trusts" "$trustCount trust(s) found"
            } catch {
                Record-Fail "Domain trusts" "Get-ADTrust error - $($_.Exception.Message)"
            }
        } else {
            Record-Skip "Domain trusts" "neither nltest nor AD module available"
        }
    }
}

# -- 13. Time Sync ----------------------------------------------------------

function Test-TimeSync {
    Write-Section "Time"

    if (Test-CommandExists "w32tm") {
        try {
            $output = w32tm /monitor /computers:$($script:DC) /nowarn 2>&1 | Out-String
            if ($output -match "NTP:\s*([+-]?\d+\.\d+)s") {
                $skew = [math]::Abs([double]$Matches[1])
                $skewStr = "{0:N2}" -f $skew
                if ($skew -le 5.0) {
                    Record-Pass "Time sync" "skew ${skewStr}s against $($script:DC)"
                } else {
                    Record-Fail "Time sync" "skew ${skewStr}s exceeds 5s threshold"
                }
            } elseif ($output -match "error|timeout|unreachable" ) {
                Record-Fail "Time sync" "w32tm could not reach $($script:DC)"
            } else {
                Record-Pass "Time sync" "w32tm completed against $($script:DC)"
            }
        } catch {
            Record-Fail "Time sync" "w32tm error - $($_.Exception.Message)"
        }
    } else {
        Record-Skip "Time sync" "w32tm not available"
    }
}

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

function Write-Header {
    if ($OutputFormat -eq "tap") {
        Write-Host "TAP version 13"
    } else {
        Write-Host ""
        Write-Color "Windows AD Smoke Tests" "White"
        Write-Host "DC:     $($script:DC)"
        Write-Host "Domain: $($script:DomainFQDN)"
        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  $($script:DC)  ($($script:DomainFQDN))" "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
# ============================================================================

# Resolve targets
$script:DC = Resolve-DomainController
$script:DomainFQDN = Resolve-Domain

Write-Header

# Run all tests
Test-DCConnectivity
Test-LDAPBind
Test-LDAPS
Test-DNSSrvRecords
Test-ADReplication
Test-FSMORoles
Test-SYSVOLShare
Test-NETLOGONShare
Test-Kerberos
Test-GroupPolicy
Test-ADWS
Test-DomainTrusts
Test-TimeSync

Write-Summary

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