param(
    [Parameter(Mandatory = $true)][string] $ExpectedVersion,
    [Parameter(Mandatory = $true)][string] $SourceDir,
    [Parameter(Mandatory = $true)][string] $BuildDir,
    [ValidateSet('Debug', 'Release', 'All')][string] $Configuration = 'All',
    [switch] $SelfTest
)

$ErrorActionPreference = 'Stop'
$SourceDir = [System.IO.Path]::GetFullPath($SourceDir)
$BuildDir = [System.IO.Path]::GetFullPath($BuildDir)
if($ExpectedVersion -notmatch '^\d+\.\d+\.\d+$'){throw 'Version must have exactly three canonical components'}

function Assert-Equal([string] $Actual, [string] $Expected, [string] $Description) {
    if ($Actual -cne $Expected) {
        throw "$Description mismatch: expected '$Expected', got '$Actual'."
    }
}

function Get-Sha256([string] $Path) {
    $stream = [System.IO.File]::OpenRead($Path)
    try {
        $sha = [System.Security.Cryptography.SHA256]::Create()
        try {
            return ([System.BitConverter]::ToString($sha.ComputeHash($stream))).Replace('-', '')
        }
        finally {
            $sha.Dispose()
        }
    }
    finally {
        $stream.Dispose()
    }
}

function Get-BundleManifest([string] $Root) {
    if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
        throw "Missing VST3 bundle: $Root"
    }

    return @(Get-ChildItem -LiteralPath $Root -File -Recurse | Sort-Object FullName | ForEach-Object {
        $relative = $_.FullName.Substring($Root.Length).TrimStart('\', '/')
        "$relative|$($_.Length)|$(Get-Sha256 $_.FullName)"
    })
}

function Test-PhysicalBinary([string]$Path,[string]$Description){
    if(-not(Test-Path -LiteralPath $Path -PathType Leaf)){throw "Missing artifact: $Path"}
    $info=(Get-Item -LiteralPath $Path).VersionInfo
    $parts=@($ExpectedVersion.Split('.') | ForEach-Object {[int]$_})+@(0)
    if($info.FileVersion -notin @($ExpectedVersion,"$ExpectedVersion.0")){throw "$Description wrong textual FileVersion"}
    if($info.ProductVersion -notin @($ExpectedVersion,"$ExpectedVersion.0")){throw "$Description wrong textual ProductVersion"}
    Assert-Equal (@($info.FileMajorPart,$info.FileMinorPart,$info.FileBuildPart,$info.FilePrivatePart)-join ',') ($parts-join ',') "$Description physical FileVersion tuple"
    Assert-Equal (@($info.ProductMajorPart,$info.ProductMinorPart,$info.ProductBuildPart,$info.ProductPrivatePart)-join ',') ($parts-join ',') "$Description physical ProductVersion tuple"
    $reader=[IO.BinaryReader]::new([IO.File]::OpenRead($Path))
    try{
        if($reader.ReadUInt16() -ne 0x5a4d){throw "Not a PE artifact: $Path"}
        $reader.BaseStream.Position=0x3c
        $offset=$reader.ReadInt32()
        if($offset -lt 0 -or $offset+6 -ge $reader.BaseStream.Length){throw 'Invalid PE offset'}
        $reader.BaseStream.Position=$offset
        if($reader.ReadUInt32() -ne 0x4550 -or $reader.ReadUInt16() -ne 0x8664){throw "Artifact is not native x64: $Path"}
    }finally{$reader.Dispose()}
}
function Test-ModuleInfo($Info,[string]$Description){
    Assert-Equal $Info.Name 'DeepPluck' "$Description name"
    Assert-Equal $Info.Version $ExpectedVersion "$Description module version"
    Assert-Equal $Info.'Factory Info'.Vendor 'ToneTrax' "$Description factory vendor"
    Assert-Equal $Info.'Factory Info'.URL '' "$Description factory URL"
    Assert-Equal $Info.'Factory Info'.'E-Mail' '' "$Description factory email"
    Assert-Equal $Info.'Factory Info'.Flags.Unicode 'True' "$Description factory Unicode flag"
    Assert-Equal $Info.'Factory Info'.Flags.'Classes Discardable' 'False' "$Description factory discardable flag"
    Assert-Equal $Info.'Factory Info'.Flags.'Component Non Discardable' 'False' "$Description component discardable flag"
    if($Info.Classes.Count -ne 2){throw "$Description class count changed"}
    $ids=@('ABCDEF019182FAEB546E747844705631','ABCDEF011234ABCD546E747844705631')
    $categories=@('Audio Module Class','Component Controller Class')
    for($i=0;$i -lt 2;$i++){
        $class=$Info.Classes[$i]
        Assert-Equal $class.CID $ids[$i] "$Description class CID"
        Assert-Equal $class.Category $categories[$i] "$Description class category"
        Assert-Equal $class.Name 'DeepPluck' "$Description class name"
        Assert-Equal $class.Vendor 'ToneTrax' "$Description class vendor"
        Assert-Equal $class.Version $ExpectedVersion "$Description class version"
        Assert-Equal $class.SDKVersion 'VST 3.8.0' "$Description unchanged SDK metadata"
        Assert-Equal ($class.'Sub Categories' -join '|') 'Instrument|Synth' "$Description subcategories"
        Assert-Equal $class.'Class Flags' '2' "$Description class flags"
        Assert-Equal $class.Cardinality '2147483647' "$Description cardinality"
        if('Snapshots' -notin $class.PSObject.Properties.Name -or $class.Snapshots.Count -ne 0){throw "$Description snapshot identity changed"}
    }
}

if($SelfTest){
    # Synthetic metadata only. No real binary or preserved reference is changed.
    $fixture=[pscustomobject]@{Name='DeepPluck';Version=$ExpectedVersion;'Factory Info'=[pscustomobject]@{Vendor='ToneTrax';URL='';'E-Mail'='';Flags=[pscustomobject]@{Unicode=$true;'Classes Discardable'=$false;'Component Non Discardable'=$false}};Classes=@()}
    foreach($entry in @(@('ABCDEF019182FAEB546E747844705631','Audio Module Class'),@('ABCDEF011234ABCD546E747844705631','Component Controller Class'))){
        $fixture.Classes+=[pscustomobject]@{CID=$entry[0];Category=$entry[1];Name='DeepPluck';Vendor='ToneTrax';Version=$ExpectedVersion;SDKVersion='VST 3.8.0';'Sub Categories'=@('Instrument','Synth');'Class Flags'=2;Cardinality=2147483647;Snapshots=@()}
    }
    Test-ModuleInfo $fixture 'positive synthetic fixture'
    $fixture.Version='999.0.0';$failed=$false
    try{Test-ModuleInfo $fixture 'stale synthetic fixture'}catch{$failed=$true}
    if(-not $failed){throw 'Stale version negative probe did not fail'}
    $fixture.Version=$ExpectedVersion;$fixture.Classes[1].Version='999.0.0';$failed=$false
    try{Test-ModuleInfo $fixture 'wrong controller synthetic fixture'}catch{$failed=$true}
    if(-not $failed){throw 'Class version negative probe did not fail'}
    $failed=$false
    try{Test-PhysicalBinary (Join-Path $BuildDir ('nonexistent-artifact-'+[guid]::NewGuid().ToString('N')+'.exe')) 'missing synthetic fixture'}catch{$failed=$true}
    if(-not $failed){throw 'Missing artifact negative probe did not fail'}
    Write-Host 'PASS: stale/missing/class-version negative synthetic probes'
    exit 0
}

function Test-Configuration([string] $Config) {
    $buildRoot = Join-Path $BuildDir "DeepPluck_artefacts/$Config"
    $localRoot = Join-Path $SourceDir "Artifacts/$Config"
    $buildVst = Join-Path $buildRoot 'VST3/DeepPluck.vst3'
    $localVst = Join-Path $localRoot 'DeepPluck.vst3'
    $buildExe = Join-Path $buildRoot 'Standalone/DeepPluck.exe'
    $localExe = Join-Path $localRoot 'DeepPluck.exe'
    $moduleInfo = Join-Path $buildVst 'Contents/Resources/moduleinfo.json'

    foreach ($required in @($buildExe, $localExe, $moduleInfo)) {
        if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
            throw "Missing artifact: $required"
        }
    }

    $moduleText=Get-Content -LiteralPath $moduleInfo -Raw
    # Steinberg's manifest writer emits JSON with trailing commas; remove only
    # these structural commas before Windows PowerShell's JSON parser.
    $module=($moduleText -replace ',\s*([}\]])','$1') | ConvertFrom-Json
    Test-ModuleInfo $module "$Config VST3"
    foreach($binary in @($buildExe,$localExe,(Join-Path $buildVst 'Contents/x86_64-win/DeepPluck.vst3'),(Join-Path $localVst 'Contents/x86_64-win/DeepPluck.vst3'))){
        Test-PhysicalBinary $binary "$Config plugin/Standalone"
    }
    foreach($target in @('DeepPluckTests','DeepPluckToneAudit','DeepPluckBenchmark','DeepPluckPresetAudit','DeepPluckPresetGenerator','DeepPluckPresetAudition')){
        Test-PhysicalBinary (Join-Path $BuildDir "${target}_artefacts/$Config/${target}.exe") "$Config $target"
    }
    $cmake=[IO.File]::ReadAllText((Join-Path $SourceDir 'CMakeLists.txt'))
    if($cmake -notmatch ('project\(DeepPluck VERSION '+[regex]::Escape($ExpectedVersion)+' LANGUAGES')){throw 'Wrong source version'}
    if($cmake -notmatch 'COPY_PLUGIN_AFTER_BUILD FALSE'){throw 'Automatic system installation must remain disabled'}

    Assert-Equal (Get-Sha256 $localExe) `
                 (Get-Sha256 $buildExe) `
                 "$Config Standalone local copy"

    $buildManifest = Get-BundleManifest $buildVst
    $localManifest = Get-BundleManifest $localVst
    Assert-Equal ($localManifest -join "`n") ($buildManifest -join "`n") "$Config VST3 local copy"

    $sourceVersionLiterals = Get-ChildItem -LiteralPath (Join-Path $SourceDir 'Source') `
        -Recurse -File -Include '*.cpp', '*.h' | Select-String -Pattern ([regex]::Escape($ExpectedVersion))
    if ($sourceVersionLiterals) {
        throw "Current version is hard-coded in Source instead of propagated from CMake."
    }

    Write-Host "PASS: $Config artifacts are byte-identical and version $ExpectedVersion"
}

$configurations = if ($Configuration -eq 'All') { @('Debug', 'Release') } else { @($Configuration) }
foreach ($config in $configurations) {
    Test-Configuration $config
}
