#Requires -Version 5.1

[CmdletBinding()]
param(
    [string] $NodePath
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Resolve-NodePath {
    param([AllowEmptyString()][string] $ExplicitPath)

    if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) {
        if (-not [IO.Path]::IsPathRooted($ExplicitPath) -or
            -not (Test-Path -LiteralPath $ExplicitPath -PathType Leaf)) {
            throw 'NodePath must identify an existing absolute node executable path.'
        }

        return [IO.Path]::GetFullPath($ExplicitPath)
    }

    $command = Get-Command node -CommandType Application -ErrorAction SilentlyContinue |
        Select-Object -First 1
    if ($null -ne $command) {
        return $command.Source
    }

    $programFiles = [Environment]::GetFolderPath(
        [Environment+SpecialFolder]::ProgramFiles)
    $candidates = @(
        (Join-Path $programFiles 'Microsoft Visual Studio\18\Community\MSBuild\Microsoft\VisualStudio\NodeJs\node.exe'),
        (Join-Path $programFiles 'Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VisualStudio\NodeJs\node.exe')
    )
    foreach ($candidate in $candidates) {
        if (Test-Path -LiteralPath $candidate -PathType Leaf) {
            return [IO.Path]::GetFullPath($candidate)
        }
    }

    throw 'A Node.js executable was not found. Pass its absolute path with -NodePath.'
}

$node = Resolve-NodePath -ExplicitPath $NodePath
$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$scripts = @(
    (Join-Path $repositoryRoot 'Web\playout-safety.js'),
    (Join-Path $repositoryRoot 'Web\market-nav-reorder.js'),
    (Join-Path $repositoryRoot 'Web\operator-workflow.js'),
    (Join-Path $repositoryRoot 'Web\legacy-fixed-workflow.js'),
    (Join-Path $repositoryRoot 'Web\industry-workflow.js'),
    (Join-Path $repositoryRoot 'Web\comparison-workflow.js'),
    (Join-Path $repositoryRoot 'Web\comparison-import-workflow.js'),
    (Join-Path $repositoryRoot 'Web\theme-workflow.js'),
    (Join-Path $repositoryRoot 'Web\overseas-workflow.js'),
    (Join-Path $repositoryRoot 'Web\expert-workflow.js'),
    (Join-Path $repositoryRoot 'Web\trading-halt-workflow.js'),
    (Join-Path $repositoryRoot 'Web\candle-options-workflow.js'),
    (Join-Path $repositoryRoot 'Web\manual-lists-workflow.js'),
    (Join-Path $repositoryRoot 'Web\manual-financial-workflow.js'),
    (Join-Path $repositoryRoot 'Web\named-manual-restore-workflow.js'),
    (Join-Path $repositoryRoot 'Web\named-comparison-restore-workflow.js'),
    (Join-Path $repositoryRoot 'Web\named-foreign-stock-restore-workflow.js'),
    (Join-Path $repositoryRoot 'Web\legacy-foreign-index-candle-workflow.js'),
    (Join-Path $repositoryRoot 'Web\named-krx-theme-restore-workflow.js'),
    (Join-Path $repositoryRoot 'Web\named-nxt-theme-restore-workflow.js'),
    (Join-Path $repositoryRoot 'Web\legacy-named-row-workflow.js'),
    (Join-Path $repositoryRoot 'Web\operator-catalog-workflow.js'),
    (Join-Path $repositoryRoot 'Web\manual-lists-ui.js'),
    (Join-Path $repositoryRoot 'Web\manual-financial-ui.js'),
    (Join-Path $repositoryRoot 'Web\operator-catalog-ui.js'),
    (Join-Path $repositoryRoot 'Web\named-playlist-workflow.js'),
    (Join-Path $repositoryRoot 'Web\app.js')
)
$testFiles = @(
    Get-ChildItem -LiteralPath (Join-Path $repositoryRoot 'tests\Web') -Filter '*.test.cjs' -File |
        Sort-Object -Property Name |
        Select-Object -ExpandProperty FullName
)
if ($testFiles.Count -eq 0) {
    throw 'No Web playout test files were found.'
}

foreach ($script in $scripts) {
    & $node --check $script
    if ($LASTEXITCODE -ne 0) {
        throw "JavaScript syntax validation failed with exit code $LASTEXITCODE."
    }
}

$appScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'Web\app.js') -Raw -Encoding UTF8
$playoutSafetyScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'Web\playout-safety.js') -Raw -Encoding UTF8
$indexMarkup = Get-Content -LiteralPath (Join-Path $repositoryRoot 'Web\index.html') -Raw -Encoding UTF8
$namedPlaylistScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'Web\named-playlist-workflow.js') -Raw -Encoding UTF8
$mainWindowScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'MainWindow.xaml.cs') -Raw -Encoding UTF8
$namedPlaylistBridgeScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'MainWindow.NamedPlaylists.cs') -Raw -Encoding UTF8
$pagePlanBridgeScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'MainWindow.PlaylistPagePlans.cs') -Raw -Encoding UTF8
$legacyWorkflowScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.Core\Playout\Scenes\LegacyPlayoutWorkflow.cs') -Raw -Encoding UTF8
$backgroundScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'MainWindow.Background.cs') -Raw -Encoding UTF8
$playoutMainScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'MainWindow.Playout.cs') -Raw -Encoding UTF8
$playoutExample = Get-Content -LiteralPath (Join-Path $repositoryRoot 'Config\playout.example.json') -Raw -Encoding UTF8 |
    ConvertFrom-Json
if ($playoutExample.PSObject.Properties.Name -notcontains 'maximumAutomaticRefreshesPerTakeIn' -or
    $null -ne $playoutExample.maximumAutomaticRefreshesPerTakeIn) {
    throw 'The example playout configuration must preserve the unlimited null compatibility default.'
}
if ($playoutMainScript -notmatch 'MaximumAutomaticRefreshesPerTakeIn' -or
    $playoutMainScript -notmatch 'scheduler\.HasReachedMaximum' -or
    $playoutMainScript -notmatch 'ExecuteRefreshAndTrackSuccessAsync' -or
    $playoutMainScript -notmatch 'DrainMaximumCallbackAsync' -or
    $playoutMainScript -notmatch 'refreshCompletedCount' -or
    $playoutMainScript -notmatch 'refreshMaximumCount' -or
    $playoutMainScript -notmatch 'refreshLimitReached') {
    throw 'The trusted automatic-refresh ceiling and final callback-drain status are not fully wired.'
}
if ($appScript -notmatch 'normalizeRefreshBudget' -or
    $appScript -notmatch 'formatRefreshBudgetState' -or
    $playoutSafetyScript -notmatch 'CAPPED') {
    throw 'The Web operator surface does not expose the native automatic-refresh ceiling.'
}
$sceneCatalogMatches = [regex]::Matches(
    $appScript,
    'builderKey:\s*"s[0-9]+"')
if ($sceneCatalogMatches.Count -ne 35) {
    throw "Web scene catalog must contain exactly 35 builder entries; found $($sceneCatalogMatches.Count)."
}
$selectionPresetMatches = [regex]::Matches(
    $appScript,
    '(?m)^\s+s[0-9]+:\s+\[')
if ($selectionPresetMatches.Count -ne 34 -or
    $appScript -notmatch 'Every reachable legacy builder must have a Web selection preset') {
    throw "Web scene selection presets must cover exactly 34 reachable builders; found $($selectionPresetMatches.Count)."
}
if ($appScript -notmatch 'reachableAliases\.size !== 45' -or
    $appScript -notmatch 'sceneCutAlias' -or
    $appScript -notmatch 'sceneAliases\(definition\)\.includes\(storedCode\)' -or
    $appScript -notmatch 'resolveStoredCatalogIndex') {
    throw 'Web scene selection must expose and preserve all 45 active cut aliases.'
}
if ($appScript -match 'payload\.cue' -or
    $appScript -notmatch 'payload\.playlist' -or
    $appScript -notmatch 'payload\.selectedIndex') {
    throw 'Web playout must send a PREPARE playlist snapshot and must not choose the NEXT cue.'
}
if ($appScript -notmatch 'const DEFAULT_FADE_DURATION = 6;' -or
    $appScript -notmatch 'fadeDuration: DEFAULT_FADE_DURATION') {
    throw 'Web playout default fade must match MainForm ComboDi.SelectedIndex 6.'
}
if ($appScript -notmatch 'normalizeScenePreviewFields' -or
    $appScript -notmatch 'currentPageItemCount' -or
    $appScript -notmatch 'renderSceneDataPreview') {
    throw 'Web playout must render bounded authoritative scene data and complete page state.'
}
if ($appScript -notmatch 'checkbox\.checked = item\.enabled !== false' -or
    $appScript -notmatch 'replacePlaylistEntryEnabledAt\(index, checkbox\.checked\)' -or
    $appScript -match 'item\.enabled\s*=(?!=)' -or
    $appScript -notmatch 'namedKrxThemeRestoreWorkflow\.withEnabled' -or
    $appScript -notmatch 'namedNxtThemeRestoreWorkflow\.withEnabled' -or
    $appScript -notmatch 'namedForeignStockRestoreWorkflow\.withEnabled' -or
    $appScript -notmatch 'isPlaylistSnapshotLocked' -or
    $appScript -notmatch 'pending: Boolean\(state\.playout\.pending\)' -or
    $appScript -notmatch 'outcomeUnknown: state\.playout\.sessionQuarantined') {
    throw 'Web playlist must expose the legacy active flag and freeze its native snapshot.'
}
if ($appScript -notmatch 'toggleAllEntriesEnabled' -or
    $appScript -notmatch 'deleteAllPlaylistEntries' -or
    $appScript -notmatch 'event\?\.shiftKey' -or
    $appScript -notmatch 'event\.key === "Home"' -or
    $appScript -notmatch 'event\.key === "End"') {
    throw 'Legacy playlist enabled-all, delete-all, range, Home and End controls are incomplete.'
}
if ($appScript -match '\bitem\.[A-Za-z_$][A-Za-z0-9_$]*\s*=(?!=)' -or
    $appScript -notmatch 'playlistEntryManualEditBlockReason' -or
    $appScript -notmatch 'recreatePlaylistEntryWithSceneSelection' -or
    $appScript -notmatch 'recreatePlaylistEntryWithLiveSelection' -or
    $appScript -notmatch 'replacePlaylistEntryAt\(state\.currentIndex, replacement\)') {
    throw 'Playlist scene and live-row edits must use immutable replacement and protect branded entries.'
}
if ($appScript -notmatch 'postNative\("playout-timeout-quarantine", \{ requestId, command \}\)' -or
    $appScript -notmatch 'browserCorrelationQuarantined') {
    throw 'Web response timeouts must latch the process-lifetime native quarantine.'
}
if ($appScript -match 'event\.key === "F2"\) requestPlayoutCommand' -or
    $appScript -notmatch 'notifyLegacyBackgroundShortcut' -or
    $appScript -notmatch 'legacyTakeInShortcutAction' -or
    $appScript -notmatch 'synchronizeStockCandleOptions' -or
    $appScript -notmatch 'operatorInputBuilderKeys\.has\(definition\.builderKey\)') {
    throw 'Legacy F2, F8, moving-average and typed-playlist safety integration is incomplete.'
}
if ($indexMarkup -notmatch 'id="globalMa5"' -or
    $indexMarkup -notmatch 'id="globalMa20"' -or
    $indexMarkup -notmatch '<script src="candle-options-workflow\.js"></script>' -or
    $appScript -notmatch 'MbnCandleOptionsWorkflow' -or
    $appScript -notmatch 'synchronizeGlobalCandleOptions' -or
    $appScript -notmatch 'legacy-overseas-workflow' -or
    $appScript -notmatch 'legacy-trading-halt-workflow') {
    throw 'The original global 5-day/20-day candle options are not synchronized across every candle workflow.'
}
foreach ($asset in @(
    'market-nav-reorder.js', 'manual-lists-workflow.js', 'manual-financial-workflow.js', 'named-manual-restore-workflow.js', 'named-foreign-stock-restore-workflow.js', 'legacy-foreign-index-candle-workflow.js', 'named-krx-theme-restore-workflow.js', 'named-nxt-theme-restore-workflow.js', 'legacy-named-row-workflow.js', 'operator-catalog-workflow.js',
    'manual-lists-ui.js', 'manual-financial-ui.js', 'operator-catalog-ui.js')) {
    if ($indexMarkup -notmatch [regex]::Escape("<script src=`"$asset`"></script>")) {
        throw "Operator UI script is not loaded: $asset"
    }
}
if ($appScript -notmatch 'MbnMarketNavReorder' -or
    $appScript -notmatch [regex]::Escape('marketNavReorder.createController') -or
    $appScript -notmatch [regex]::Escape('hasOpenOperatorModal() || isPlaylistSnapshotLocked()')) {
    throw 'The original business-tab drag and keyboard reorder contract is not connected.'
}
foreach ($asset in @('manual-lists-ui.css', 'manual-financial-ui.css', 'operator-catalog-ui.css')) {
    if ($indexMarkup -notmatch [regex]::Escape("<link rel=`"stylesheet`" href=`"$asset`">") ) {
        throw "Operator UI stylesheet is not loaded: $asset"
    }
}
if ($indexMarkup -notmatch 'id="themeCatalogManageButton"' -or
    $indexMarkup -notmatch 'id="expertCatalogManageButton"' -or
    $appScript -notmatch 'MbnManualListsUi' -or
    $appScript -notmatch 'MbnManualFinancialUi' -or
    $appScript -notmatch 'MbnOperatorCatalogUi' -or
    $appScript -notmatch 'initializeOperatorUiControllers' -or
    $appScript -notmatch 'appendTrustedOperatorEntry' -or
    $appScript -notmatch 'manualFinancialPrepareBlocked' -or
    $appScript -notmatch 'createRestoreSelectionRequest' -or
    $appScript -notmatch 'operatorCatalogUi\?\.loseCorrelation') {
    throw 'GraphE, FSell, VIList, ThemeA or EList is not fully connected to the Web operator surface.'
}
if ($appScript -notmatch 'requestBackgroundSelection' -or
    $appScript -notmatch 'event\.key === "F3"' -or
    $appScript -notmatch 'postNative\("request-background-status"' -or
    $mainWindowScript -notmatch 'case "choose-background"' -or
    $mainWindowScript -notmatch 'case "toggle-background"' -or
    $playoutMainScript -notmatch 'MutableLegacySceneCueCompositionOptionsSource' -or
    $backgroundScript -notmatch 'CreateOperatorSelection' -or
    $backgroundScript -notmatch 'CreateLegacyDefault' -or
    $backgroundScript -notmatch 'TryGetTrustedBackgroundDirectory') {
    throw 'Legacy F2/F3 native background selection integration is incomplete.'
}
if ($appScript -notmatch 'restoreFixedPlaylistEntry' -or
    $appScript -notmatch 'synchronizeFixedEntries' -or
    $appScript -notmatch 'renderFixedWorkflow' -or
    $appScript -notmatch 'legacy-fixed-workflow') {
    throw 'Legacy fixed operator catalog integration is incomplete.'
}
if ($appScript -notmatch 'restoreIndustryPlaylistEntry' -or
    $appScript -notmatch 'synchronizeIndustryEntries' -or
    $appScript -notmatch 'renderIndustryWorkflow' -or
    $appScript -notmatch 'postNative\("request-industries", \{ requestId, market \}\)' -or
    $mainWindowScript -notmatch 'case "request-industries"' -or
    $mainWindowScript -notmatch 'PostMessage\("industry-results"') {
    throw 'Legacy KOSPI/KOSDAQ industry selector integration is incomplete.'
}
if ($appScript -notmatch 'restoreComparisonPlaylistEntry' -or
    $appScript -notmatch 'synchronizeComparisonEntries' -or
    $appScript -notmatch 'renderComparisonWorkflow' -or
    $appScript -notmatch 'postNative\("search-world-stocks"' -or
    $mainWindowScript -notmatch 'case "search-world-stocks"' -or
    $mainWindowScript -notmatch 'PostMessage\("world-stock-search-results"') {
    throw 'Legacy comparison selector and world-stock search integration is incomplete.'
}
if ($appScript -notmatch 'restoreThemePlaylistEntry' -or
    $appScript -notmatch 'synchronizeThemeEntries' -or
    $appScript -notmatch 'renderThemeWorkflow' -or
    $appScript -notmatch 'postNative\("search-themes"' -or
    $appScript -notmatch 'postNative\("request-theme-preview"' -or
    $mainWindowScript -notmatch 'case "search-themes"' -or
    $mainWindowScript -notmatch 'case "request-theme-preview"') {
    throw 'Legacy read-only theme selector integration is incomplete.'
}
if ($appScript -notmatch 'restoreOverseasPlaylistEntry' -or
    $appScript -notmatch 'synchronizeOverseasEntries' -or
    $appScript -notmatch 'renderOverseasWorkflow' -or
    $appScript -notmatch 'postNative\(overseasWorkflow\.bridgeContracts\[kind\]\.requestType, request\)' -or
    $mainWindowScript -notmatch 'case "search-overseas-industries"' -or
    $mainWindowScript -notmatch 'PostMessage\("overseas-industry-results"') {
    throw 'Legacy overseas-industry and overseas-stock selector integration is incomplete.'
}
if ($appScript -notmatch 'restoreExpertPlaylistEntry' -or
    $appScript -notmatch 'synchronizeExpertEntries' -or
    $appScript -notmatch 'renderExpertWorkflow' -or
    $appScript -notmatch 'postNative\(expertWorkflow\.bridgeContract\.previewRequestType, request\)' -or
    $mainWindowScript -notmatch 'case "search-experts"' -or
    $mainWindowScript -notmatch 'case "request-expert-preview"') {
    throw 'Legacy read-only expert selector integration is incomplete.'
}
if ($appScript -notmatch 'restoreTradingHaltPlaylistEntry' -or
    $appScript -notmatch 'synchronizeTradingHaltEntries' -or
    $appScript -notmatch 'renderTradingHaltWorkflow' -or
    $appScript -notmatch 'postNative\(tradingHaltWorkflow\.bridgeContract\.requestType, request\)' -or
    $mainWindowScript -notmatch 'case "search-trading-halts"' -or
    $mainWindowScript -notmatch 'PostMessage\("trading-halt-results"') {
    throw 'Legacy trading-halt selector integration is incomplete.'
}
if ($indexMarkup -notmatch 'id="namedPlaylistPanel"' -or
    $indexMarkup -notmatch 'id="namedPlaylistDefinitions"' -or
    $indexMarkup -notmatch 'id="namedPlaylistCreateButton"' -or
    $indexMarkup -notmatch 'id="namedPlaylistReplaceButton"' -or
    $indexMarkup -notmatch 'id="namedPlaylistDeleteButton"' -or
    $indexMarkup -notmatch 'id="namedPlaylistPageRetryButton"' -or
    $indexMarkup -notmatch '<script src="named-playlist-workflow\.js"></script>' -or
    $indexMarkup -notmatch 'id="loadButton"' -or
    $indexMarkup -notmatch 'id="saveButton"' -or
    $indexMarkup -notmatch 'id="namedPlaylistOpenButton"') {
    throw 'Named Oracle playlist controls must remain explicit and separate from local temporary storage.'
}
if ($appScript -notmatch 'MbnNamedPlaylistWorkflow' -or
    $appScript -notmatch 'requestNamedPlaylistList' -or
    $appScript -notmatch 'requestNamedPlaylistLoad' -or
    $appScript -notmatch 'requestNamedPlaylistCreate' -or
    $appScript -notmatch 'requestNamedPlaylistReplace' -or
    $appScript -notmatch 'requestNamedPlaylistDelete' -or
    $appScript -notmatch 'namedPlaylistPrepareBlockers' -or
    $appScript -notmatch 'restoreRawRows\([\s\S]*load,[\s\S]*resolveNamedPlaylistRawRow,[\s\S]*legacyNamedRowWorkflow\.matchesCanonicalEntry' -or
    $appScript -notmatch 'requestNamedPlaylistPagePlans' -or
    $appScript -notmatch 'normalizePagePlanResponse\(payload, pending\.request\)' -or
    $appScript -notmatch 'page-result-empty' -or
    $appScript -notmatch 'namedPlaylistBackupStorageKey' -or
    $appScript -notmatch 'writeQuarantined') {
    throw 'Named Oracle playlist UI, trusted restoration, backup, PREPARE guard, or write quarantine is incomplete.'
}
if ($namedPlaylistScript -notmatch 'request-playlist-page-plans' -or
    $namedPlaylistScript -notmatch 'playlist-page-plans-results' -or
    $namedPlaylistScript -notmatch 'playlist-page-plans-error' -or
    $mainWindowScript -notmatch 'case "request-playlist-page-plans"' -or
    $pagePlanBridgeScript -notmatch 'PreflightPagePlansAsync' -or
    $pagePlanBridgeScript -notmatch 'await _databaseActivityGate\.WaitAsync\(requestToken\)' -or
    $pagePlanBridgeScript -notmatch 'Volatile\.Read\(ref _playoutCommandInFlight\) != 0' -or
    $legacyWorkflowScript -notmatch 'PreflightPagePlansAsync') {
    throw 'Read-only named-playlist page preflight, exact Web correlation, or playout-priority gating is incomplete.'
}
foreach ($requestType in @(
    'request-named-playlist-list',
    'request-named-playlist-load',
    'create-named-playlist',
    'replace-named-playlist',
    'delete-named-playlist')) {
    if ($namedPlaylistScript -notmatch [regex]::Escape($requestType) -or
        $mainWindowScript -notmatch [regex]::Escape($requestType)) {
        throw "Named playlist request contract is incomplete: $requestType"
    }
}
foreach ($responseType in @(
    'named-playlist-list-results',
    'named-playlist-load-results',
    'named-playlist-mutation-results',
    'named-playlist-error')) {
    if ($appScript -notmatch [regex]::Escape($responseType) -or
        $namedPlaylistBridgeScript -notmatch [regex]::Escape($responseType)) {
        throw "Named playlist response contract is incomplete: $responseType"
    }
}
if ($mainWindowScript -notmatch 'OnNavigationStarting' -or
    $mainWindowScript -notmatch 'OnProcessFailed' -or
    $mainWindowScript -notmatch 'ReloadBrowserSafely' -or
    $mainWindowScript -notmatch 'QuarantineIfBrowserCorrelationCanBeLost' -or
    $mainWindowScript -notmatch 'Volatile\.Read\(ref _playoutCommandInFlight\)') {
    throw 'Reload, navigation, and WebView process failure must quarantine in-flight playout correlation.'
}

$cutCoverageFixturePath = Join-Path $PSScriptRoot '..\tests\Scripts\Test-LegacyCutCoverage.Fixture.ps1'
$cutCoverageFixture = & $cutCoverageFixturePath
if ($cutCoverageFixture.Status -ne 'Passed') {
    throw 'Legacy cut and builder asset coverage fixtures failed.'
}

& $node --test @testFiles
if ($LASTEXITCODE -ne 0) {
    throw "Web playout tests failed with exit code $LASTEXITCODE."
}

[pscustomobject][ordered]@{
    Status = 'Passed'
    SyntaxFiles = $scripts.Count
    SceneCatalogEntries = $sceneCatalogMatches.Count
    ReachableSelectionPresets = $selectionPresetMatches.Count
    TestFiles = $testFiles.Count
    CutCoverageFixture = $cutCoverageFixture.Status
}
