# watchdog_vfp.ps1 - lanseaza un test VFP headless si detecteaza dialoguri native blocante
# (mesaje Windows, "View Parameter", erori modale) care nu apar in logul scris de script si
# nu sunt prinse de ON ERROR / mock-uri - cazul clasic de proces "agatat" cu CPU~0.
#
# Ce face: sterge .fxp-ul vechi, lanseaza `vfp9.exe -A -T "<script>"`, apoi polleaza la interval
# scurt ferestrele TOP-LEVEL ale procesului (EnumWindows + GetWindowThreadProcessId). Fereastra
# principala VFP se identifica prin Process.MainWindowHandle (.NET) - NU dupa numele clasei:
# VFP inregistreaza clase diferite prefixate "vfp9..." atat pentru shell-ul principal cat si
# pentru dialogurile proprii (ex. "View Parameter"), deci clasificarea pe clasa e nesigura.
# Orice fereastra NOUA, diferita de MainWindowHandle, e tratata ca dialog blocant.
#
# Pentru fiecare dialog detectat: captura PNG (PrintWindow, fallback CopyFromScreen daca iese
# neagra) + dump text (titlu, clasa, si textul/clasa fiecarui control copil via WM_GETTEXT - se
# citesc controale native Windows chiar daca apartin unui proces strain pe alta arhitectura).
#
# INTERZIS: watchdog-ul NU are voie sa foloseasca input real de tastatura/mouse (keybd_event,
# SetForegroundWindow, SendInput, mouse_event) - masina e PARTAJATA cu utilizatorul, iar un
# ESCAPE/click injectat la nivel de sistem ajunge in orice fereastra are focus in acel moment,
# nu neaparat in dialogul tintit (verificat: a ajuns in IDE-ul VFP al utilizatorului aflat in
# lucru in paralel, intrerupandu-l). -AutoDismiss foloseste STRICT mesaje Windows tintite pe
# handle-ul dialogului: buton copil cu text Cancel/Anulare/Renunta -> BM_CLICK; altfel
# WM_COMMAND IDCANCEL -> ESCAPE ca mesaj (WM_KEYDOWN/WM_KEYUP, nu input real) -> WM_CLOSE, in
# ordine, primul care inchide fereastra castiga. Dialogurile proprii VFP owner-drawn (ex.
# "View Parameter" - butoanele sunt desenate, nu HWND-uri reale) pot sa NU raspunda la niciunul -
# atunci dismiss-ul ESUEAZA CINSTIT (ramane deschis pana la timeout), nu se cade pe input real.
# Captura (PNG+dump text) e intotdeauna de incredere, indiferent de rezultatul dismiss-ului.
# Continua polling-ul dupa fiecare dismiss reusit, ca sa prinda si eventuale dialoguri urmatoare
# (pana la -MaxDialogs). Fara -AutoDismiss: captureaza primul dialog gasit, apoi opreste
# polling-ul (nu-l atinge).
#
# Procesul e omorat mereu la iesire (finally) - nu ramane niciodata viu.
#
# Utilizare:
#   powershell -ExecutionPolicy Bypass -File watchdog_vfp.ps1 -Script "D:\...\test_x.prg" -AutoDismiss
#   powershell -ExecutionPolicy Bypass -File watchdog_vfp.ps1 -Script "D:\...\test_x.prg" -TimeoutSec 60 -OutDir "D:\...\out"
#
# Parametri:
#   -Script       cale completa la .prg de rulat (obligatoriu)
#   -Vfp          cale la vfp9.exe (implicit instalarea standard x86)
#   -TimeoutSec   timeout total de polling (implicit 90)
#   -OutDir       folder pentru PNG-uri + log text (implicit "<folderul scriptului>\watchdog_out")
#   -AutoDismiss  inchide automat dialogurile gasite (Cancel/WM_CLOSE) si continua polling-ul
#   -MaxDialogs   cate dialoguri succesive sa trateze cu -AutoDismiss (implicit 10)
#
# Iesire: rezumat pe stdout (numar dialoguri, textul fiecaruia, caile PNG, exit code-ul
# procesului) + acelasi continut scris in "<Script>_watchdog.log" din -OutDir.

param(
    [Parameter(Mandatory=$true)][string]$Script,
    [string]$Vfp = 'C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe',
    [int]$TimeoutSec = 90,
    [string]$OutDir,
    [switch]$AutoDismiss,
    [int]$MaxDialogs = 10
)
$ErrorActionPreference = 'Stop'

if (-not (Test-Path $Script)) { throw "Nu gasesc scriptul: $Script" }
$Script = (Resolve-Path $Script).Path
$ScriptDir = [System.IO.Path]::GetDirectoryName($Script)
$ScriptBase = [System.IO.Path]::GetFileNameWithoutExtension($Script)
if (-not $OutDir) { $OutDir = Join-Path $ScriptDir 'watchdog_out' }
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
$LogPath = Join-Path $OutDir ($ScriptBase + '_watchdog.log')

Add-Type -AssemblyName System.Drawing

Add-Type @"
using System;
using System.Text;
using System.Runtime.InteropServices;

public class WdWin32 {
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

    [DllImport("user32.dll")]
    public static extern bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);

    [DllImport("user32.dll")]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    [DllImport("user32.dll")]
    public static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll")]
    public static extern bool IsWindow(IntPtr hWnd);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    [DllImport("user32.dll")]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto, EntryPoint = "SendMessage")]
    public static extern IntPtr SendMessageText(IntPtr hWnd, uint Msg, int wParam, StringBuilder lParam);

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT { public int Left, Top, Right, Bottom; }

    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    [DllImport("user32.dll")]
    public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
}
"@

$WM_GETTEXT = 0x000D
$WM_CLOSE   = 0x0010
$BM_CLICK   = 0x00F5

function Get-TopWindowsForPid([int]$ProcId) {
    $result = New-Object System.Collections.Generic.List[IntPtr]
    $cb = {
        param($hWnd, $lParam)
        $owner = 0
        [void][WdWin32]::GetWindowThreadProcessId($hWnd, [ref]$owner)
        if ($owner -eq $ProcId -and [WdWin32]::IsWindowVisible($hWnd)) { $result.Add($hWnd) }
        return $true
    }
    [WdWin32]::EnumWindows($cb, [IntPtr]::Zero) | Out-Null
    return $result
}

function Get-ChildWindowsOf([IntPtr]$hWndParent) {
    $result = New-Object System.Collections.Generic.List[IntPtr]
    $cb = {
        param($hWnd, $lParam)
        $result.Add($hWnd)
        return $true
    }
    [WdWin32]::EnumChildWindows($hWndParent, $cb, [IntPtr]::Zero) | Out-Null
    return $result
}

# WM_GETTEXT (nu GetWindowText) - citeste text de la controale ale unui proces strain, indiferent
# de arhitectura (32/64 bit); GetWindowText poate esua tacut pe controale custom VFP.
function Get-WmText([IntPtr]$hWnd, [int]$MaxLen = 1024) {
    $sb = New-Object System.Text.StringBuilder $MaxLen
    [WdWin32]::SendMessageText($hWnd, $WM_GETTEXT, $MaxLen, $sb) | Out-Null
    return $sb.ToString()
}

function Get-WinClassName([IntPtr]$hWnd) {
    $sb = New-Object System.Text.StringBuilder 256
    [WdWin32]::GetClassName($hWnd, $sb, $sb.Capacity) | Out-Null
    return $sb.ToString()
}

function Test-BitmapAllBlack([System.Drawing.Bitmap]$bmp) {
    $w = $bmp.Width; $h = $bmp.Height
    if ($w -le 0 -or $h -le 0) { return $true }
    for ($i = 0; $i -lt 9; $i++) {
        $x = [Math]::Min($w - 1, [int]($w * (($i % 3) + 0.5) / 3))
        $y = [Math]::Min($h - 1, [int]($h * ([Math]::Floor($i / 3) + 0.5) / 3))
        $px = $bmp.GetPixel($x, $y)
        if ($px.R -ne 0 -or $px.G -ne 0 -or $px.B -ne 0) { return $false }
    }
    return $true
}

# PrintWindow direct pe handle; fallback CopyFromScreen daca iese neagra (fereastra themed/GDI+
# care nu se randeaza corect prin PrintWindow). Foloseste doar pentru dialoguri surprinse
# on-screen - nu se muta off-screen aici (spre deosebire de vfp_ui_harness.ps1), fiindca scopul
# e sa surprindem exact dialogul care blocheaza, o singura data.
function Save-DialogScreenshot([IntPtr]$hWnd, [string]$Path) {
    $rect = New-Object WdWin32+RECT
    [WdWin32]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
    $w = $rect.Right - $rect.Left
    $h = $rect.Bottom - $rect.Top
    if ($w -le 0 -or $h -le 0) { return $false }
    $bmp = New-Object System.Drawing.Bitmap $w, $h
    $g = [System.Drawing.Graphics]::FromImage($bmp)
    $hdc = $g.GetHdc()
    $ok = [WdWin32]::PrintWindow($hWnd, $hdc, 2)
    $g.ReleaseHdc($hdc)
    $g.Dispose()
    if ((-not $ok) -or (Test-BitmapAllBlack $bmp)) {
        $bmp.Dispose()
        $bmp = New-Object System.Drawing.Bitmap $w, $h
        $g2 = [System.Drawing.Graphics]::FromImage($bmp)
        try { $g2.CopyFromScreen($rect.Left, $rect.Top, 0, 0, (New-Object System.Drawing.Size $w, $h)) } catch {}
        $g2.Dispose()
    }
    $bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
    $bmp.Dispose()
    return $true
}

function Get-DialogDump([IntPtr]$hWnd, [System.Collections.Generic.List[IntPtr]]$children) {
    $title = Get-WmText $hWnd
    $class = Get-WinClassName $hWnd
    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add("dialog hwnd=$hWnd class='$class' title='$title'")
    foreach ($c in $children) {
        $ct = (Get-WmText $c 512).Trim()
        $cc = Get-WinClassName $c
        if ($ct -or $cc) { $lines.Add("  child hwnd=$c class='$cc' text='$ct'") }
    }
    # unary comma obligatoriu: fara ea, un List[string] cu UN SINGUR element (dialog fara copii,
    # ex. "View Parameter") e "descompus" de PowerShell la scalar string simplu la return, iar
    # indexarea ulterioara $dumpLines[0] citeste primul CARACTER, nu primul rand - prins la testul
    # pe test_page3_articole.prg (rezumatul arata titlul "d" in loc de linia intreaga)
    return , $lines.ToArray()
}

function Find-DismissButton([System.Collections.Generic.List[IntPtr]]$children) {
    foreach ($c in $children) {
        if ((Get-WinClassName $c) -ne 'Button') { continue }
        $txt = ((Get-WmText $c 256) -replace '&', '').Trim()
        if ($txt -ieq 'Cancel' -or $txt -ieq 'Anulare' -or $txt -ieq 'Renunta' -or $txt -ieq 'Renunță') { return $c }
    }
    return [IntPtr]::Zero
}

$WM_COMMAND = 0x0111
$WM_KEYDOWN = 0x0100
$WM_KEYUP   = 0x0101
$VK_ESCAPE  = 0x1B
$IDCANCEL   = 2

# INTERZIS: input real de tastatura/mouse (keybd_event, SetForegroundWindow, SendInput,
# mouse_event). Masina e PARTAJATA cu utilizatorul - un ESCAPE injectat la nivel de sistem
# ajunge in orice fereastra are focus in acel moment, INDIFERENT carui proces ii apartine
# (verificat: a ajuns in IDE-ul VFP al utilizatorului, aflat in lucru in paralel, intrerupandu-l
# cu o eroare + "Execution was canceled by the user"). Foloseste STRICT mesaje Windows tintite pe
# HANDLE-ul dialogului (PostMessage/SendMessage: WM_COMMAND IDCANCEL, WM_KEYDOWN/WM_KEYUP
# VK_ESCAPE, WM_CLOSE, BM_CLICK) - astea nu ating focusul/inputul global, raman izolate la
# fereastra tintita. Dialogurile native VFP proprii (ex. "View Parameter") NU au controale copil
# reale (owner-drawn) si pot sa NU raspunda la niciunul dintre mesajele de mai jos - in acel caz
# dismiss-ul ESUEAZA CINSTIT (dialogul ramane deschis, procesul e omorat la timeout de finally),
# nu se cade pe input real. Captura (PNG+dump text) ramane oricum de incredere, indiferent de
# rezultatul dismiss-ului.
function Dismiss-Dialog([IntPtr]$hWnd, [System.Collections.Generic.List[IntPtr]]$children) {
    $btn = Find-DismissButton $children
    if ($btn -ne [IntPtr]::Zero) {
        [WdWin32]::SendMessage($btn, $BM_CLICK, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null
        Start-Sleep -Milliseconds 300
        if (-not [WdWin32]::IsWindow($hWnd)) { return 'click Cancel/Anulare' }
        return 'NEREUSIT: BM_CLICK pe Cancel/Anulare nu a inchis dialogul'
    }

    [WdWin32]::SendMessage($hWnd, $WM_COMMAND, [IntPtr]$IDCANCEL, [IntPtr]::Zero) | Out-Null
    Start-Sleep -Milliseconds 300
    if (-not [WdWin32]::IsWindow($hWnd)) { return 'WM_COMMAND IDCANCEL' }

    [WdWin32]::SendMessage($hWnd, $WM_KEYDOWN, [IntPtr]$VK_ESCAPE, [IntPtr]::Zero) | Out-Null
    [WdWin32]::SendMessage($hWnd, $WM_KEYUP, [IntPtr]$VK_ESCAPE, [IntPtr]::Zero) | Out-Null
    Start-Sleep -Milliseconds 300
    if (-not [WdWin32]::IsWindow($hWnd)) { return 'tasta ESCAPE trimisa cu SendMessage (mesaj, nu input real)' }

    [WdWin32]::SendMessage($hWnd, $WM_CLOSE, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null
    Start-Sleep -Milliseconds 300
    if (-not [WdWin32]::IsWindow($hWnd)) { return 'WM_CLOSE' }

    $motiv = if ($children.Count -eq 0) { 'fara controale copil (owner-drawn) si niciun mesaj tintit nu a functionat' } else { 'niciun mesaj tintit (WM_COMMAND/ESCAPE mesaj/WM_CLOSE) nu a functionat' }
    return "NEREUSIT: dialogul a ramas deschis ($motiv) - fara input real, per regula masinii partajate"
}

# omoara doar vfp9 lansate pe ACEST script (linia de comanda contine calea lui), nu instante straine
function Stop-VfpForScript([string]$ScriptPath) {
    Get-CimInstance Win32_Process -Filter "Name='vfp9.exe'" -ErrorAction SilentlyContinue | ForEach-Object {
        if ($_.CommandLine -and $_.CommandLine.ToLower().Contains($ScriptPath.ToLower())) {
            Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
        }
    }
}

# --- pregatire ---
$Fxp = [System.IO.Path]::ChangeExtension($Script, 'fxp')
if (Test-Path $Fxp) { Remove-Item $Fxp -Force }
Stop-VfpForScript $Script
Start-Sleep -Milliseconds 300

$log = New-Object System.Collections.Generic.List[string]
function Log([string]$s) { Write-Host $s; $log.Add($s) }

Log "watchdog_vfp: lansez $Vfp -A -T `"$Script`""
$proc = Start-Process -FilePath $Vfp -ArgumentList @('-A', '-T', "`"$Script`"") -PassThru

$dialogs = New-Object System.Collections.Generic.List[object]
$exitCode = $null

try {
    # Fereastra principala = Process.MainWindowHandle (.NET), latch-uita o singura data. NU se
    # poate distinge dupa numele clasei: VFP inregistreaza clase diferite prefixate "vfp9..." atat
    # pentru shell-ul principal ("vfp99400000") CAT SI pentru dialoguri native proprii precum
    # "View Parameter" ("vfp994000002") - verificat empiric, clasificarea pe clasa a lasat
    # "View Parameter" nedetectat. MainWindowHandle e acelasi mecanism folosit deja cu succes in
    # vfp_ui_harness.ps1 (Wait-MainWindowHandle) pentru shell-ul VFP.
    $mainHwnd = [IntPtr]::Zero
    $mwDeadline = (Get-Date).AddSeconds(20)
    while ($mainHwnd -eq [IntPtr]::Zero -and (Get-Date) -lt $mwDeadline) {
        $proc.Refresh()
        if ($proc.HasExited) { break }
        if ($proc.MainWindowHandle -ne [IntPtr]::Zero) { $mainHwnd = $proc.MainWindowHandle }
        else { Start-Sleep -Milliseconds 200 }
    }
    if ($mainHwnd -ne [IntPtr]::Zero) {
        Log ("fereastra principala VFP: hwnd={0} class='{1}' title='{2}'" -f $mainHwnd, (Get-WinClassName $mainHwnd), (Get-WmText $mainHwnd))
    } else {
        Log "ATENTIE: MainWindowHandle nu a aparut in 20s - orice fereastra gasita va fi tratata ca dialog."
    }

    $knownWindows = New-Object System.Collections.Generic.HashSet[IntPtr]
    if ($mainHwnd -ne [IntPtr]::Zero) { [void]$knownWindows.Add($mainHwnd) }
    $deadline = (Get-Date).AddSeconds($TimeoutSec)
    $stop = $false

    while (-not $stop -and (Get-Date) -lt $deadline) {
        $proc.Refresh()
        if ($proc.HasExited) { Log "proces terminat singur (exit code $($proc.ExitCode))"; break }

        # daca MainWindowHandle inca nu era prins, incearca din nou (fereastra principala poate
        # aparea mai tarziu decat asteptarea initiala de 20s in cazuri lente)
        if ($mainHwnd -eq [IntPtr]::Zero -and $proc.MainWindowHandle -ne [IntPtr]::Zero) {
            $mainHwnd = $proc.MainWindowHandle
            [void]$knownWindows.Add($mainHwnd)
            Log ("fereastra principala VFP (intarziata): hwnd={0} class='{1}' title='{2}'" -f $mainHwnd, (Get-WinClassName $mainHwnd), (Get-WmText $mainHwnd))
        }

        $windows = Get-TopWindowsForPid $proc.Id

        foreach ($w in $windows) {
                if ($knownWindows.Contains($w)) { continue }
                [void]$knownWindows.Add($w)

                if ($w -eq $mainHwnd) { continue }

                $children = Get-ChildWindowsOf $w
                $dumpLines = Get-DialogDump $w $children
                $idx = $dialogs.Count
                $png = Join-Path $OutDir ("{0}_dialog{1}.png" -f $ScriptBase, $idx)
                Save-DialogScreenshot $w $png | Out-Null

                Log "=== DIALOG $idx detectat ==="
                foreach ($l in $dumpLines) { Log $l }
                Log "  screenshot: $png"

                $dismissMethod = $null
                if ($AutoDismiss) {
                    $dismissMethod = Dismiss-Dialog $w $children
                    Log "  dismiss: $dismissMethod"
                }

                $dialogs.Add([pscustomobject]@{
                    Index    = $idx
                    Title    = ($dumpLines[0])
                    Png      = $png
                    Dismiss  = $dismissMethod
                    DumpText = ($dumpLines -join "`r`n")
                })

                if ($dialogs.Count -ge $MaxDialogs) { Log "MaxDialogs ($MaxDialogs) atins."; $stop = $true; break }
                if (-not $AutoDismiss) { Log "AutoDismiss oprit - opresc polling-ul dupa primul dialog."; $stop = $true; break }
        }

        if (-not $stop) { Start-Sleep -Milliseconds 700 }
    }

    if (-not $proc.HasExited -and (Get-Date) -ge $deadline) {
        Log "TIMEOUT (${TimeoutSec}s) - procesul e inca viu, il opresc."
    }

    $proc.Refresh()
    if ($proc.HasExited) { $exitCode = $proc.ExitCode }
}
finally {
    if ($proc -and -not $proc.HasExited) {
        try { $proc.Kill() } catch {}
    }
    Stop-VfpForScript $Script
}

# --- rezumat ---
Log ""
Log "=== REZUMAT ==="
Log ("dialoguri detectate: {0}" -f $dialogs.Count)
foreach ($d in $dialogs) {
    Log ("  [{0}] {1}" -f $d.Index, $d.Title)
    Log ("      png: {0}" -f $d.Png)
    if ($d.Dismiss) { Log ("      dismiss: {0}" -f $d.Dismiss) }
}
Log ("exit code proces: {0}" -f ($(if ($null -ne $exitCode) { $exitCode } else { '(omorat de watchdog, nu s-a terminat singur)' })))
Log ("log complet: {0}" -f $LogPath)

[System.IO.File]::WriteAllLines($LogPath, $log)
