Invoke-VBDNSEnrichmentTestRun.ps1

# ============================================================
# SCRIPT : Invoke-VBDNSEnrichmentTestRun.ps1
# VERSION : 1.0.0
# AUTHOR : VB
# PURPOSE : Top-level orchestration wrapper for the VB.DNSEnrichment
# test suite. Accepts CSV input, provisions or mounts a
# SQLite database interactively, auto-detects environment
# capabilities (DHCP role, AD, SNMP), then delegates every
# test section to Test-VBDNSEnrichmentModule.ps1.
# Output is tee'd to a timestamped log file next to the DB.
# A per-IP summary CSV is written on completion.
# REQUIRES : VB.DNSEnrichment v0.4.0, PSSQLite
# ENCODING : UTF-8 with BOM
# ============================================================

#Requires -Version 5.1

[CmdletBinding()]
param(
    # ---- Input -------------------------------------------------
    [Parameter(Mandatory)]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })]
    [string]$CsvPath,

    # ---- DB provisioning (prompted if omitted) -----------------
    [Parameter()]
    [string]$DatabaseFolder,

    [Parameter()]
    [string]$DatabaseName,

    # ---- Optional env overrides --------------------------------
    # If the DHCP Server Role is detected locally it is auto-populated.
    # Supply this only when the role lives on a remote server.
    [Parameter()]
    [string]$DHCPServer,

    # SNMP community strings — defaults used if not supplied.
    [Parameter()]
    [string[]]$SNMPCommunityStrings = @('public'),

    # Switch IPs for Layer 10 — left empty if not supplied.
    [Parameter()]
    [string[]]$SwitchTargets = @(),

    # ---- Test behaviour ----------------------------------------
    [Parameter()]
    [switch]$SkipActiveProbes,

    [Parameter()]
    [switch]$ForceRefresh
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ScriptStart = Get-Date

# ============================================================
# REGION: Helpers
# ============================================================

function Write-Banner {
    param([string]$Text, [string]$Colour = 'White')
    Write-Host ""
    Write-Host " $Text" -ForegroundColor $Colour -BackgroundColor DarkBlue
    Write-Host ""
}

function Write-Step {
    param([string]$Text)
    Write-Host " >> $Text" -ForegroundColor Cyan
}

function Write-Info {
    param([string]$Text)
    Write-Host " $Text" -ForegroundColor Gray
}

function Write-Warn {
    param([string]$Text)
    Write-Host " [!] $Text" -ForegroundColor Yellow
}

function Write-Err {
    param([string]$Text)
    Write-Host " [X] $Text" -ForegroundColor Red
}

function Prompt-WithDefault {
    param(
        [string]$Prompt,
        [string]$Default = ''
    )
    $display = if ($Default) { "$Prompt [$Default]: " } else { "$Prompt: " }
    $answer  = Read-Host -Prompt $display
    if ([string]::IsNullOrWhiteSpace($answer)) { $Default } else { $answer.Trim() }
}

# ============================================================
# REGION: CSV ingestion
# ============================================================

Write-Banner "VB.DNSEnrichment -- Test Orchestrator | $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" 'White'

Write-Step "Reading CSV: $CsvPath"
try {
    $csvRows = Import-Csv -Path $CsvPath -Encoding UTF8
} catch {
    Write-Err "Failed to read CSV: $($_.Exception.Message)"
    exit 1
}

if ($csvRows.Count -eq 0) {
    Write-Err "CSV is empty -- nothing to process."
    exit 1
}

# Auto-detect IP column
$ipColumnCandidates = @('IPAddress','IP_Address','IP Address','IP','ip_address','ip address','ipaddress')
$ipColumn = $null
$firstRow  = $csvRows | Select-Object -First 1
foreach ($candidate in $ipColumnCandidates) {
    if ($firstRow.PSObject.Properties.Name -contains $candidate) {
        $ipColumn = $candidate
        break
    }
}

if (-not $ipColumn) {
    Write-Warn "Could not auto-detect IP column. Available columns:"
    $firstRow.PSObject.Properties.Name | ForEach-Object { Write-Info " - $_" }
    $ipColumn = Prompt-WithDefault -Prompt "Enter the exact column name containing IP addresses"
    if (-not ($firstRow.PSObject.Properties.Name -contains $ipColumn)) {
        Write-Err "Column '$ipColumn' not found in CSV."
        exit 1
    }
}

Write-Info "Using IP column: '$ipColumn'"

$rawIPs = $csvRows | ForEach-Object { $_.$ipColumn } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }

if ($rawIPs.Count -eq 0) {
    Write-Err "No IP addresses found in column '$ipColumn'."
    exit 1
}

# Deduplicate
$IPAddresses = $rawIPs | Sort-Object -Unique
Write-Info "Loaded $($IPAddresses.Count) unique IP(s) from $($csvRows.Count) CSV row(s)."

# ============================================================
# REGION: Database provisioning
# ============================================================

Write-Step "Database setup"

if (-not $DatabaseFolder) {
    $DatabaseFolder = Prompt-WithDefault -Prompt "Enter folder path for the SQLite database" -Default (Join-Path $env:USERPROFILE 'Desktop\VBEnrichmentDB')
}

if (-not (Test-Path -LiteralPath $DatabaseFolder)) {
    Write-Info "Folder does not exist -- creating: $DatabaseFolder"
    try {
        New-Item -ItemType Directory -Path $DatabaseFolder -Force | Out-Null
    } catch {
        Write-Err "Cannot create folder: $($_.Exception.Message)"
        exit 1
    }
}

if (-not $DatabaseName) {
    $DatabaseName = Prompt-WithDefault -Prompt "Enter the database filename (without extension)" -Default 'VBEnrichment'
}

# Normalise: strip .db extension if the user typed it, then re-add
$DatabaseName = [System.IO.Path]::GetFileNameWithoutExtension($DatabaseName)
$dbPath = Join-Path $DatabaseFolder "$DatabaseName.db"

if (Test-Path -LiteralPath $dbPath) {
    Write-Info "Existing database found -- will mount: $dbPath"
} else {
    Write-Info "No database at that path -- will create: $dbPath"
}

# ============================================================
# REGION: Environment auto-detection
# ============================================================

Write-Step "Detecting local environment capabilities"

# DHCP Server Role
if (-not $DHCPServer) {
    $dhcpRolePresent = $false
    try {
        $dhcpService = Get-Service -Name 'DHCPServer' -ErrorAction SilentlyContinue
        if ($dhcpService -and $dhcpService.Status -eq 'Running') {
            $dhcpRolePresent = $true
        }
    } catch {
        # Service query failed -- role absent
    }

    if ($dhcpRolePresent) {
        $DHCPServer = $env:COMPUTERNAME
        Write-Info "DHCP Server role detected on this machine -- using: $DHCPServer"
    } else {
        $dhcpInput = Prompt-WithDefault -Prompt "DHCP Server FQDN or IP (leave blank to skip)" -Default ''
        if (-not [string]::IsNullOrWhiteSpace($dhcpInput)) {
            $DHCPServer = $dhcpInput
        }
    }
}

# AD availability
$adAvailable = $false
try {
    $adModule = Get-Module -Name ActiveDirectory -ListAvailable -ErrorAction SilentlyContinue
    if ($adModule) {
        $adAvailable = $true
        Write-Info "ActiveDirectory module found -- AD layer will be active."
    } else {
        Write-Info "ActiveDirectory module not found -- AD layer will be skipped."
    }
} catch {
    Write-Info "Could not check for ActiveDirectory module -- AD layer may be skipped."
}

# PSSQLite check
$psSqliteAvailable = $null -ne (Get-Module -Name PSSQLite -ListAvailable -ErrorAction SilentlyContinue)
if (-not $psSqliteAvailable) {
    Write-Err "PSSQLite module is not installed. Install it with: Install-Module PSSQLite -Scope CurrentUser"
    exit 1
}
Write-Info "PSSQLite available."

# SNMP availability hint (best-effort)
$snmpHint = $false
try {
    $snmpSvc = Get-Service -Name 'SNMP' -ErrorAction SilentlyContinue
    if ($snmpSvc) { $snmpHint = $true }
} catch { }
Write-Info "SNMP service present on this host: $snmpHint (module detects full availability at runtime)"

# Summary
Write-Host ""
Write-Host (" {0,-28} {1}" -f "CSV file:",$CsvPath)          -ForegroundColor Gray
Write-Host (" {0,-28} {1}" -f "IPs to test:",$IPAddresses.Count) -ForegroundColor Gray
Write-Host (" {0,-28} {1}" -f "Database path:",$dbPath)       -ForegroundColor Gray
Write-Host (" {0,-28} {1}" -f "DHCP Server:",$(if($DHCPServer){$DHCPServer}else{'(not configured)'})) -ForegroundColor Gray
Write-Host (" {0,-28} {1}" -f "SNMP community strings:",($SNMPCommunityStrings -join ', ')) -ForegroundColor Gray
Write-Host (" {0,-28} {1}" -f "Switch targets:",$(if($SwitchTargets.Count -gt 0){$SwitchTargets -join ', '}else{'(none)'})) -ForegroundColor Gray
Write-Host (" {0,-28} {1}" -f "SkipActiveProbes:",$SkipActiveProbes.IsPresent) -ForegroundColor Gray
Write-Host (" {0,-28} {1}" -f "ForceRefresh:",$ForceRefresh.IsPresent) -ForegroundColor Gray
Write-Host ""

$confirm = Prompt-WithDefault -Prompt "Proceed with these settings? (Y/N)" -Default 'Y'
if ($confirm -notmatch '^[Yy]') {
    Write-Warn "Aborted by user."
    exit 0
}

# ============================================================
# REGION: Log file setup (tee to file)
# ============================================================

$logFile = Join-Path $DatabaseFolder ("TestRun_{0}.log" -f (Get-Date -Format 'yyyyMMdd_HHmmss'))
Write-Step "Logging output to: $logFile"

# Start transcript to capture all console output
try {
    Start-Transcript -Path $logFile -Append -NoClobber | Out-Null
} catch {
    Write-Warn "Could not start transcript: $($_.Exception.Message)"
}

# ============================================================
# REGION: Build context -- inject DB path
# ============================================================

Write-Step "Loading VB.DNSEnrichment module"

$modulePsd1 = Join-Path $PSScriptRoot 'VB.DNSEnrichment.psd1'
if (-not (Test-Path -LiteralPath $modulePsd1)) {
    Write-Err "Module manifest not found at: $modulePsd1"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}

try {
    Import-Module $modulePsd1 -Force -ErrorAction Stop
    Write-Info "Module loaded: v$((Get-Module VB.DNSEnrichment).Version)"
} catch {
    Write-Err "Module import failed: $($_.Exception.Message)"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}

# Build context params
$ctxParams = @{
    DatabasePath         = $dbPath
    SNMPCommunityStrings = $SNMPCommunityStrings
    SwitchTargets        = $SwitchTargets
    Quiet                = $true
}
if ($DHCPServer) { $ctxParams['DHCPServer'] = $DHCPServer }

Write-Step "Building enrichment context"
try {
    $ctx = Get-VBEnrichmentContext @ctxParams
    Write-Info "Context ready PSEdition=$($ctx.PSEdition) CanUseParallel=$($ctx.CanUseParallel)"
    Write-Info " AD=$($ctx.ADAvailable) DHCP=$($ctx.DHCPAvailable) SNMP=$($ctx.SNMPAvailable) mDNS=$($ctx.mDNSAvailable)"
} catch {
    Write-Err "Get-VBEnrichmentContext failed: $($_.Exception.Message)"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}

# Initialize (or verify) the database
Write-Step "Initialising database"
try {
    Initialize-VBEnrichmentDatabase -DatabasePath $dbPath
    Write-Info "Database ready: $dbPath"
} catch {
    Write-Err "Database initialisation failed: $($_.Exception.Message)"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}

# ============================================================
# REGION: Delegate to Test-VBDNSEnrichmentModule.ps1
# ============================================================

Write-Step "Launching test suite (Test-VBDNSEnrichmentModule.ps1)"
Write-Host ""

$testScript = Join-Path $PSScriptRoot 'Test-VBDNSEnrichmentModule.ps1'
if (-not (Test-Path -LiteralPath $testScript)) {
    Write-Err "Test script not found: $testScript"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}

$testParams = @{
    IPAddress    = $IPAddresses
    OutputPath   = $DatabaseFolder
    ForceRefresh = $ForceRefresh
}
if ($DHCPServer)              { $testParams['DHCPServer']            = $DHCPServer }
if ($SNMPCommunityStrings)    { $testParams['SNMPCommunityStrings']  = $SNMPCommunityStrings }
if ($SwitchTargets.Count -gt 0) { $testParams['SwitchTargets']      = $SwitchTargets }
if ($SkipActiveProbes)        { $testParams['SkipActiveProbes']      = $true }

$testExitCode = 0
try {
    & $testScript @testParams
    $testExitCode = $LASTEXITCODE
} catch {
    Write-Err "Test script threw an exception: $($_.Exception.Message)"
    $testExitCode = 1
}

# ============================================================
# REGION: Database update verification + summary CSV export
# ============================================================

Write-Host ""
Write-Step "Verifying database state and exporting summary"

$summaryPath = $null
try {
    $dbRows = Get-VBEnrichmentResult -IPAddress $IPAddresses -Context $ctx

    # IPs that enrichment did not produce a DB row for (e.g. public IPs rejected)
    $storedIPs   = @($dbRows | ForEach-Object { $_.IPAddress })
    $missingIPs  = $IPAddresses | Where-Object { $storedIPs -notcontains $_ }

    Write-Info "IPs requested : $($IPAddresses.Count)"
    Write-Info "IPs in DB : $($storedIPs.Count)"

    if ($missingIPs.Count -gt 0) {
        Write-Warn "$($missingIPs.Count) IP(s) have no DB row (likely rejected as non-private or errored):"
        $missingIPs | ForEach-Object { Write-Info " - $_" }
    }

    # For IPs that were already in the DB but may have stale data, run a targeted upsert
    # to ensure the DB reflects the freshest results from this test run.
    if ($storedIPs.Count -gt 0 -and -not $ForceRefresh) {
        Write-Info "Running targeted refresh upsert for $($storedIPs.Count) DB row(s)..."
        try {
            $refreshResults = Invoke-VBIPEnrichment -IPAddress $storedIPs -Context $ctx -ForceRefresh
            $upsertCount = @($refreshResults).Count
            Write-Info "Upsert complete -- $upsertCount row(s) updated in DB."
        } catch {
            Write-Warn "Upsert pass failed: $($_.Exception.Message)"
        }
    }

    # Re-query after upsert to get the final state for export
    $summaryRows = Get-VBEnrichmentResult -IPAddress $IPAddresses -Context $ctx

    if ($summaryRows -and @($summaryRows).Count -gt 0) {
        $summaryPath = Join-Path $DatabaseFolder ("EnrichmentSummary_{0}.csv" -f (Get-Date -Format 'yyyyMMdd_HHmmss'))
        $summaryRows | Export-VBEnrichmentResult -Format CSV -Path $summaryPath
        Write-Info "Summary CSV written ($(@($summaryRows).Count) rows): $summaryPath"
    } else {
        Write-Warn "No enrichment rows in DB for the tested IPs -- summary CSV skipped."
    }
} catch {
    Write-Warn "DB verification / export step failed: $($_.Exception.Message)"
}

# ============================================================
# REGION: Final banner
# ============================================================

$elapsed = [int](New-TimeSpan -Start $ScriptStart -End (Get-Date)).TotalSeconds
Write-Host ""
Write-Host (" " + ("=" * 60)) -ForegroundColor DarkGray
$statusColour = if ($testExitCode -ne 0) { 'Red' } else { 'Green' }
$statusText   = if ($testExitCode -ne 0) { 'COMPLETED WITH FAILURES' } else { 'ALL TESTS PASSED' }
Write-Host (" $statusText | {0}s elapsed" -f $elapsed) -ForegroundColor $statusColour
Write-Host (" DB : $dbPath") -ForegroundColor Gray
Write-Host (" Log : $logFile") -ForegroundColor Gray
if ($summaryPath -and (Test-Path -LiteralPath $summaryPath -ErrorAction SilentlyContinue)) {
    Write-Host (" CSV : $summaryPath") -ForegroundColor Gray
}
Write-Host (" " + ("=" * 60)) -ForegroundColor DarkGray
Write-Host ""

Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
exit $testExitCode