param(
  [int]$GatewayPort = 18889,
  [string]$GatewayToken = 'qjc-bundled-runtime-token',
  [int]$SmokePort = 4318,
  [string]$SmokeToken = 'smoke-token',
  [string]$BaseOutputDir,
  [string]$ModelConfigFile,
  [int]$TimeoutSeconds = 240,
  [switch]$SkipMaterializeRuntime
)

$ErrorActionPreference = 'Stop'

function Write-Utf8File {
  param([string]$FilePath, [string]$Content)
  $encoding = New-Object System.Text.UTF8Encoding $false
  [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($FilePath)) | Out-Null
  [System.IO.File]::WriteAllText($FilePath, $Content, $encoding)
}

function Test-TcpPortAvailable {
  param([int]$Port)

  $listener = $null
  try {
    $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $Port)
    $listener.Start()
    return $true
  } catch {
    return $false
  } finally {
    if ($listener) {
      $listener.Stop()
    }
  }
}

function Get-FreeTcpPort {
  $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), 0)
  try {
    $listener.Start()
    return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
  } finally {
    $listener.Stop()
  }
}

function Get-FirstMatchValue {
  param(
    [string]$Text,
    [string[]]$Patterns
  )

  foreach ($pattern in $Patterns) {
    $match = [regex]::Match($Text, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
    if ($match.Success) {
      return $match.Groups[1].Value.Trim()
    }
  }

  return ''
}

function Get-MatchValues {
  param(
    [string]$Text,
    [string]$Pattern
  )

  $values = New-Object System.Collections.Generic.List[string]
  $matches = [regex]::Matches($Text, $Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
  foreach ($match in $matches) {
    $candidate = if ($match.Groups.Count -gt 1) { $match.Groups[1].Value.Trim() } else { "" }
    if ($candidate) {
      $values.Add($candidate)
    }
  }

  return @($values.ToArray())
}

function Get-IndexedValue {
  param(
    [string[]]$Values,
    [int]$Index
  )

  if ($Values.Count -gt $Index) {
    return $Values[$Index]
  }

  return ""
}

function Resolve-SmokeSettingsConfig {
  param([string]$ConfigFile)

  if (-not $ConfigFile) {
    $desktopPath = [Environment]::GetFolderPath("Desktop")
    $defaultFileName = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("6b6Z6Jm+5a+G6ZKlJuaooeWei+mFjee9ruS/oeaBry50eHQ="))
    $ConfigFile = Join-Path $desktopPath $defaultFileName
  }

  if (-not $ConfigFile -or -not (Test-Path -LiteralPath $ConfigFile)) {
    return $null
  }

  $raw = Get-Content -LiteralPath $ConfigFile -Raw -Encoding UTF8
  $baseUrls = Get-MatchValues -Text $raw -Pattern '(?m)^\s*(?:baseUrl|base_url|ARK_BASE_URL)\s*[:=\uFF1A]\s*(\S+)'
  $apiKeys = Get-MatchValues -Text $raw -Pattern '(?m)^\s*(?:api_key|apiKey|ARK_API_KEY)\s*[:=\uFF1A]\s*(\S+)'
  $models = Get-MatchValues -Text $raw -Pattern '(?m)^\s*(?:model|modelId|VIDEO_LLM_ANALYZER_MODEL)\s*[:=\uFF1A]\s*(\S+)'

  $copywriting = [ordered]@{
    baseUrl = Get-IndexedValue -Values $baseUrls -Index 0
    apiKey = Get-IndexedValue -Values $apiKeys -Index 0
    modelId = Get-IndexedValue -Values $models -Index 0
  }
  $video = [ordered]@{
    baseUrl = Get-IndexedValue -Values $baseUrls -Index 1
    apiKey = Get-IndexedValue -Values $apiKeys -Index 1
    modelId = Get-IndexedValue -Values $models -Index 1
  }
  $image = [ordered]@{
    baseUrl = Get-IndexedValue -Values $baseUrls -Index 2
    apiKey = Get-IndexedValue -Values $apiKeys -Index 2
    modelId = Get-IndexedValue -Values $models -Index 2
  }

  if (-not $copywriting.apiKey -or -not $copywriting.baseUrl -or -not $copywriting.modelId) {
    throw "Smoke model config file did not contain a complete copywriting model config: $ConfigFile"
  }

  return [ordered]@{
    expertModelConfig = [ordered]@{
      copywriting = $copywriting
      video = $video
      image = $image
      digitalHuman = [ordered]@{
        volcAccessKey = Get-FirstMatchValue -Text $raw -Patterns @('OMNIHUMAN_VOLC_ACCESS_KEY\s*=\s*(\S+)')
        volcSecretKey = Get-FirstMatchValue -Text $raw -Patterns @('OMNIHUMAN_VOLC_SECRET_KEY\s*=\s*(\S+)')
        qiniuAccessKey = Get-FirstMatchValue -Text $raw -Patterns @('OMNIHUMAN_QINIU_ACCESS_KEY\s*=\s*(\S+)')
        qiniuSecretKey = Get-FirstMatchValue -Text $raw -Patterns @('OMNIHUMAN_QINIU_SECRET_KEY\s*=\s*(\S+)')
      }
    }
    douyinRuntimeConfig = [ordered]@{
      replicationBrief = [ordered]@{
        baseUrl = Get-IndexedValue -Values $baseUrls -Index 3
        apiKey = Get-IndexedValue -Values $apiKeys -Index 3
        modelId = Get-IndexedValue -Values $models -Index 3
      }
      videoAnalyzer = [ordered]@{
        baseUrl = Get-IndexedValue -Values $baseUrls -Index 4
        apiKey = Get-IndexedValue -Values $apiKeys -Index 4
        modelId = Get-IndexedValue -Values $models -Index 4
      }
    }
  }
}
function New-ExpertFixtureProject {
  param(
    [string]$ProjectsRoot,
    [string]$ProjectId,
    [string]$ProjectName,
    [string]$Platform,
    [string]$Description,
    [string]$ReadmeBody,
    [string]$UpdatedAt
  )

  $projectRoot = Join-Path $ProjectsRoot $ProjectId
  New-Item -ItemType Directory -Force -Path $projectRoot, (Join-Path $projectRoot 'memory') | Out-Null

  $projectPayload = [ordered]@{
    id = $ProjectId
    name = $ProjectName
    description = $Description
    platform = $Platform
    ready = $true
    updatedAt = $UpdatedAt
    boundSkillIds = @()
  }

  Write-Utf8File (Join-Path $projectRoot 'project.json') ($projectPayload | ConvertTo-Json -Depth 8)
  Write-Utf8File (Join-Path $projectRoot 'README.md') ("# $ProjectName`n`n$ReadmeBody")
}

function Initialize-SmokeUserData {
  param([string]$UserDataPath)

  $projectsRoot = Join-Path $UserDataPath 'projects'
  $manifestsRoot = Join-Path $UserDataPath 'manifests'
  if (Test-Path $UserDataPath) {
    Remove-Item $UserDataPath -Recurse -Force -ErrorAction SilentlyContinue
  }
  New-Item -ItemType Directory -Force -Path $UserDataPath, $projectsRoot, $manifestsRoot | Out-Null

  New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'content-account-planning-smoke' -ProjectName 'Content Account Planning Expert Workspace' -Platform 'content-account-planning' -Description 'Standalone expert fixture for account planning UI smoke.' -ReadmeBody 'Used to validate bootstrap prompt injection in the real desktop UI flow.' -UpdatedAt '2026-04-16T00:00:00.000Z'
  New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'zhihu-smoke' -ProjectName 'Zhihu Expert Workspace' -Platform 'zhihu' -Description 'Standalone expert fixture for Zhihu UI smoke.' -ReadmeBody 'Used to validate bootstrap prompt injection in the real desktop UI flow.' -UpdatedAt '2026-04-16T00:01:00.000Z'
  New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'douyin-expert-smoke' -ProjectName 'Douyin Expert Workspace' -Platform 'douyin' -Description 'Control fixture to keep workspace non-trivial.' -ReadmeBody 'Ensures workspace still contains other expert-like projects.' -UpdatedAt '2026-04-16T00:02:00.000Z'

  Write-Utf8File (Join-Path $manifestsRoot 'active-project.json') (@{ projectId = 'douyin-expert-smoke' } | ConvertTo-Json -Depth 3)
}

function Invoke-BootstrapPromptScenario {
  param(
    [string]$ScenarioName,
    [string]$SmokeExpertEntryId,
    [string]$Prompt,
    [string]$ExpectedPromptSnippet,
    [string]$BaseOutputDir,
    [string]$ElectronSmokeScript,
    [int]$SmokePort,
    [string]$SmokeToken,
    [int]$TimeoutSeconds
  )

  $scenarioRoot = Join-Path $BaseOutputDir $ScenarioName
  $userDataPath = Join-Path $scenarioRoot 'user-data'
  $logsPath = Join-Path $scenarioRoot 'logs'
  $smokeOutput = Join-Path $scenarioRoot 'result.json'

  Initialize-SmokeUserData -UserDataPath $userDataPath
  if (Test-Path $logsPath) {
    Remove-Item $logsPath -Recurse -Force -ErrorAction SilentlyContinue
  }
  New-Item -ItemType Directory -Force -Path $scenarioRoot, $logsPath | Out-Null

  $previousStreamTimeout = $env:QJCLAW_SMOKE_STREAM_TIMEOUT_MS
  $env:QJCLAW_SMOKE_STREAM_TIMEOUT_MS = [string]([Math]::Max(120000, ($TimeoutSeconds - 30) * 1000))
  powershell -ExecutionPolicy Bypass -File $ElectronSmokeScript @(
    '-SmokeOutput', $smokeOutput,
    '-SmokePort', $SmokePort,
    '-SmokeToken', $SmokeToken,
    '-UserDataPath', $userDataPath,
    '-LogsPath', $logsPath,
    '-RuntimeMode', 'bundled-runtime',
    '-ExpectBundledRuntime',
    '-PreserveUserData',
    '-SmokeViewMode', 'chat',
    '-SmokeExpertEntryId', $SmokeExpertEntryId,
    '-SmokeSendAfterExpertEntry',
    '-SmokePrompt', $Prompt,
    '-TimeoutSeconds', $TimeoutSeconds
  )
  if ($null -eq $previousStreamTimeout) {
    Remove-Item Env:QJCLAW_SMOKE_STREAM_TIMEOUT_MS -ErrorAction SilentlyContinue
  } else {
    $env:QJCLAW_SMOKE_STREAM_TIMEOUT_MS = $previousStreamTimeout
  }
  if ($LASTEXITCODE -ne 0) {
    exit $LASTEXITCODE
  }

  $summaryScript = @"
const fs = require('fs');
const [scenarioName, smokeOutput, expectedExpertEntryId, expectedPrompt, expectedPromptSnippet] = process.argv.slice(1);
const result = JSON.parse(fs.readFileSync(smokeOutput, 'utf8'));
if (!result.ok) {
  throw new Error(result.error || (scenarioName + ' smoke failed.'));
}
const sendResult = result.sendResult || {};
const finalState = result.finalState || {};
const streamSmoke = sendResult.streamSmoke || {};
const expertEntry = sendResult.expertEntry || {};
const assistantContent = String((sendResult.lastAssistantMessage && sendResult.lastAssistantMessage.content) || streamSmoke.finalContent || streamSmoke.renderedContent || '');
if (String(sendResult.smokeExpertEntryId || '') !== expectedExpertEntryId) {
  throw new Error('Smoke did not report the expected expert entry id: ' + String(sendResult.smokeExpertEntryId || ''));
}
if (String(sendResult.smokeExpertEntryAction || '') !== 'activate-and-send') {
  throw new Error('Smoke expert entry action mismatch: ' + String(sendResult.smokeExpertEntryAction || ''));
}
if (String(expertEntry.entryMode || '') !== 'standalone') {
  throw new Error('Expert entryMode mismatch: ' + String(expertEntry.entryMode || ''));
}
if (String(finalState.viewMode || '') !== 'experts') {
  throw new Error('Final viewMode mismatch: ' + String(finalState.viewMode || ''));
}
if (String(streamSmoke.phase || '') !== 'completed') {
  throw new Error('Stream did not complete: ' + String(streamSmoke.phase || ''));
}
if (!String(streamSmoke.prompt || '').includes(expectedPrompt)) {
  throw new Error('Stream smoke did not record the submitted prompt.');
}
if (String(streamSmoke.executionPolicySource || '') !== 'client-config') {
  throw new Error('Stream did not use the saved client model config.');
}
if (String(streamSmoke.executionPolicyModel || '') !== 'qwen3.6-plus') {
  throw new Error('Stream did not use the expected copywriting model.');
}
console.log(JSON.stringify({
  ok: true,
  scenarioName,
  smokeOutput,
  expertEntryId: expectedExpertEntryId,
  currentProjectId: expertEntry.currentProjectId || null,
  sessionId: sendResult.sessionId || streamSmoke.sessionId || null,
  streamPhase: streamSmoke.phase || null,
  executionPolicySource: streamSmoke.executionPolicySource || null,
  executionPolicyModel: streamSmoke.executionPolicyModel || null,
  promptRecorded: String(streamSmoke.prompt || '').includes(expectedPrompt),
  bootstrapPromptObserved: assistantContent ? assistantContent.includes(expectedPromptSnippet) : null
}, null, 2));
"@

  $summary = & node -e $summaryScript $ScenarioName $smokeOutput $SmokeExpertEntryId $Prompt $ExpectedPromptSnippet
  if ($LASTEXITCODE -ne 0) {
    exit $LASTEXITCODE
  }

  Write-Output $summary
}

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if (-not $BaseOutputDir) {
  $BaseOutputDir = Join-Path $repoRoot '.tmp\desktop-expert-bootstrap-ui-smoke'
}
$BaseOutputDir = [System.IO.Path]::GetFullPath($BaseOutputDir)
$electronSmokeScript = Join-Path $repoRoot 'build\scripts\electron-smoke.ps1'
$smokeSettingsConfig = Resolve-SmokeSettingsConfig -ConfigFile $ModelConfigFile
$gatewayPortWasExplicit = $PSBoundParameters.ContainsKey("GatewayPort")
if (-not $gatewayPortWasExplicit -and -not (Test-TcpPortAvailable -Port $GatewayPort)) {
  $fallbackGatewayPort = Get-FreeTcpPort
  Write-Host "Gateway port $GatewayPort is busy; using isolated smoke gateway port $fallbackGatewayPort"
  $GatewayPort = $fallbackGatewayPort
}
$contentPlanningPromptFile = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('5YaF5a656LSm5Y+36KeE5YiS5LiT5a62cHJvbXB0Lm1k'))
$zhihuPromptFile = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('55+l5LmO5LiT5a62cHJvbXB0Lm1k'))
$contentPlanningSnippet = (Get-Content -Path (Join-Path $repoRoot (Join-Path 'apps\desktop\bootstrap\prompts' $contentPlanningPromptFile)) -Encoding UTF8 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1).Trim()
$zhihuSnippet = (Get-Content -Path (Join-Path $repoRoot (Join-Path 'apps\desktop\bootstrap\prompts' $zhihuPromptFile)) -Encoding UTF8 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1).Trim()

if (Test-Path $BaseOutputDir) {
  Remove-Item $BaseOutputDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $BaseOutputDir | Out-Null
$runtimeDir = if ($GatewayPort -eq 18889) {
  Join-Path $repoRoot 'vendor\openclaw-runtime'
} else {
  Join-Path $BaseOutputDir "runtime-$GatewayPort"
}

if ($smokeSettingsConfig) {
  $env:QJCLAW_SMOKE_SETTINGS_CONFIG_JSON = ($smokeSettingsConfig | ConvertTo-Json -Depth 10 -Compress)
}

if (-not $SkipMaterializeRuntime) {
  Write-Host "Materializing bundled runtime payload on port $GatewayPort"
  powershell -ExecutionPolicy Bypass -File (Join-Path $repoRoot 'build\scripts\materialize-runtime-payload.ps1') -RuntimeDir $runtimeDir -GatewayPort $GatewayPort -GatewayToken $GatewayToken
  if ($LASTEXITCODE -ne 0) {
    exit $LASTEXITCODE
  }
}

$env:QJCLAW_SMOKE_RUNTIME_DIR = $runtimeDir

$contentPlanningPrompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('VUkgc21va2U6IOivt+W4ruaIkeinhOWIkuS4gOS4quWuoOeJqeS4u+eQhuS6uuWGheWuuei0puWPt+OAgg=='))
Invoke-BootstrapPromptScenario -ScenarioName 'standalone-content-account-planning-bootstrap' -SmokeExpertEntryId 'content-account-planning' -Prompt $contentPlanningPrompt -ExpectedPromptSnippet $contentPlanningSnippet -BaseOutputDir $BaseOutputDir -ElectronSmokeScript $electronSmokeScript -SmokePort $SmokePort -SmokeToken $SmokeToken -TimeoutSeconds $TimeoutSeconds

$zhihuPrompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('VUkgc21va2U6IOivt+W4ruaIkeWGmeS4gOS4quefpeS5juWbnuetlOW8gOWktOOAgg=='))
Invoke-BootstrapPromptScenario -ScenarioName 'standalone-zhihu-bootstrap' -SmokeExpertEntryId 'zhihu' -Prompt $zhihuPrompt -ExpectedPromptSnippet $zhihuSnippet -BaseOutputDir $BaseOutputDir -ElectronSmokeScript $electronSmokeScript -SmokePort $SmokePort -SmokeToken $SmokeToken -TimeoutSeconds $TimeoutSeconds

Remove-Item Env:QJCLAW_SMOKE_SETTINGS_CONFIG_JSON -ErrorAction SilentlyContinue
Remove-Item Env:QJCLAW_SMOKE_RUNTIME_DIR -ErrorAction SilentlyContinue
