'use strict'

const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const test = require('node:test')

const root = path.resolve(__dirname, '..', '..')
const read = relativePath => fs.readFileSync(
  path.join(root, relativePath),
  'utf8'
)
const { readStylesCss } = require('../helpers/css-source.js')

test('debug UI exposes the complete stepping and inspection surface', () => {
  const html = read('index.html')
  const requiredIds = [
    'debug-pause',
    'debug-step-into',
    'debug-step',
    'debug-step-out',
    'debug-continue',
    'debug-threads',
    'debug-call-stack',
    'debug-variable-list',
    'debug-watch-list',
    'debug-exception-list'
  ]

  for (const id of requiredIds) {
    assert.match(html, new RegExp(`id=["']${id}["']`))
  }
})

test('debug shortcuts and renderer commands cover pause and stepping', () => {
  const shortcuts = read('js/shortcuts.js')
  const services = read('js/services.js')
  const debugInterpreter = read('js/debug-interpreter.js')

  assert.match(shortcuts, /id:\s*'stepInto'[\s\S]*?defaultValue:\s*'F7'/)
  assert.match(shortcuts, /id:\s*'stepOut'[\s\S]*?defaultValue:\s*'Shift\+F8'/)
  assert.match(shortcuts, /id:\s*'pause'[\s\S]*?scope:\s*'debug'/)
  assert.match(debugInterpreter, /sendDebugCommand\('pause'/)
  assert.match(debugInterpreter, /sendDebugCommand\('stepIn'/)
  assert.match(debugInterpreter, /sendDebugCommand\('stepOut'/)
  assert.match(
    services,
    /watchExpressions:\s*window\.debugWatchExpressions/
  )
})

test('debug IPC exposes evaluate and exception breakpoint channels', () => {
  const debugModule = read('js/main/cangjie-debug.js')
  const preload = read('preload.js')

  for (const channel of [
    'cangjie-debug-command',
    'cangjie-debug-evaluate',
    'cangjie-debug-exceptions'
  ]) {
    assert.match(debugModule, new RegExp(`ipcMain\\.handle\\('${channel}'`))
    assert.match(preload, new RegExp(`'${channel}'`))
  }
})

test('debug console input has a dedicated renderer-to-debuggee channel', () => {
  const debugModule = read('js/main/cangjie-debug.js')
  const preload = read('preload.js')
  const services = read('js/services.js')

  assert.match(debugModule, /ipcMain\.on\('cangjie-debug-stdin'/)
  assert.match(preload, /'cangjie-debug-stdin'/)
  assert.match(
    services,
    /window\.ipcRenderer\.send\('cangjie-debug-stdin',\s*\{\s*text\s*\}\)/
  )
})

test('debug console REPL and setVariable expose a complete cross-process surface', () => {
  const debugModule = read('js/main/cangjie-debug.js')
  const preload = read('preload.js')
  const client = read('js/cangjie-debug-client.js')
  const html = read('index.html')
  const services = read('js/services.js')
  const debugInterpreter = read('js/debug-interpreter.js')

  // set-variable 三件套:主进程 handler + preload 白名单 + 渲染端调用
  assert.match(debugModule, /ipcMain\.handle\('cangjie-debug-set-variable'/)
  assert.match(preload, /'cangjie-debug-set-variable'/)
  assert.match(
    services,
    /window\.ipcRenderer\.invoke\(\s*'cangjie-debug-set-variable'/
  )

  // evaluate 通道支持 repl 上下文(后端能力位 + 渲染端入口)
  assert.match(debugModule, /payload\?\.context === 'repl'/)
  assert.match(
    client,
    /async evaluate\(expression, frameId = this\.frameId, context = 'watch'\)/
  )
  assert.match(
    client,
    /async setVariable\(name, value, variablesReference = 0\)/
  )
  assert.match(
    client,
    /supportsSetVariable: this\.adapterCapabilities\?\.supportsSetVariable === true/
  )

  // 渲染端:Console 标签、调试控制台输出区、REPL 输入与回显
  assert.match(html, /data-phase="debug"/)
  assert.match(html, /id="svc-debug-console"/)
  assert.match(html, /id="svc-debug-input"/)
  assert.match(services, /window\.appendDebugConsole = function/)
  assert.match(debugInterpreter, /context: 'repl'/)
  assert.match(
    debugInterpreter,
    /window\.focusDebugConsoleInput = function/
  )

  // 变量编辑受能力位门控,避免适配器不支持时误开入口
  assert.match(services, /window\.debugCanSetVariable === true/)
  assert.match(
    debugInterpreter,
    /window\.debugCanSetVariable = !!\(\s*capabilities && capabilities\.supportsSetVariable === true/
  )
})

test('function breakpoints expose a complete cross-process surface', () => {
  const client = read('js/cangjie-debug-client.js')
  const debugModule = read('js/main/cangjie-debug.js')
  const services = read('js/services.js')
  const debugInterpreter = read('js/debug-interpreter.js')
  const conditional = read('js/conditional-breakpoints.js')
  const html = read('index.html')

  // 后端:能力位 + 归一化 + DAP 请求 + start 链路 + command 重发
  assert.match(
    client,
    /supportsFunctionBreakpoints: this\.adapterCapabilities\?\.supportsFunctionBreakpoints === true/
  )
  assert.match(client, /normalizeFunctionBreakpoints\(functions\)/)
  assert.match(client, /async setFunctionBreakpoints\(functions = \[\]\)/)
  assert.match(client, /this\.request\('setFunctionBreakpoints'/)
  assert.match(
    client,
    /await this\.setFunctionBreakpoints\(functionBreakpoints\)/
  )
  assert.match(
    client,
    /action === 'continue'[\s\S]*?requestOptions\.functionBreakpoints[\s\S]*?setFunctionBreakpoints/
  )

  // IPC:command handler 透传 functionBreakpoints
  assert.match(
    debugModule,
    /functionBreakpoints:\s*Array\.isArray\(request\.functionBreakpoints\)/
  )

  // 渲染端模型与 API
  assert.match(services, /window\.debugFunctionBreakpoints\s*=/)
  assert.match(
    services,
    /window\.collectDebugFunctionBreakpoints = function/
  )
  assert.match(
    services,
    /window\.addDebugFunctionBreakpoint = addDebugFunctionBreakpoint/
  )
  assert.match(
    services,
    /window\.removeDebugFunctionBreakpoint = removeDebugFunctionBreakpoint/
  )

  // start / command 携带
  assert.match(
    debugInterpreter,
    /functionBreakpoints:\s*window\.collectDebugFunctionBreakpoints/
  )
  assert.match(debugInterpreter, /commandPayload\.functionBreakpoints\s*=/)

  // gutter 菜单入口
  assert.match(conditional, /Add Function Breakpoint/)
  assert.match(conditional, /detectCangjieFunctionNameAtLine/)
  assert.match(conditional, /window\.addDebugFunctionBreakpoint/)

  // 渲染容器
  assert.match(html, /id="debug-function-breakpoint-list"/)
})

test('large debug values expose an expanded JSON hover surface', () => {
  const debugInterpreter = read('js/debug-interpreter.js')
  const hoverProvider = read('js/monaco/debug-hover-provider-init.js')
  const hoverLayout = read('js/monaco/debug-hover-layout-init.js')
  const styles = readStylesCss()

  // 摘要 → JSON 两阶段模型,集合显示元素数、对象显示字段数
  assert.match(debugInterpreter, /formatDebugVariableJson/)
  assert.match(debugInterpreter, /itemCount:\s*getDebugVariableCount\(variable\)/)
  assert.match(debugInterpreter, /stage:\s*expanded\s*\?\s*'json'/)
  assert.match(debugInterpreter, /language:\s*expanded\s*\?\s*'json'/)

  // 旧版 Monaco hover 卡片已由自定义悬浮框接管:provider 必须显式返回 null,
  // 不再注册 command:cangjie.debug.expandVariable 链接动作
  assert.match(hoverProvider, /return null/)
  assert.doesNotMatch(hoverProvider, /cangjie\.debug\.expandVariable/)

  // 自定义悬浮框展开/收起 API 与样式容器。独立的"查看完整 JSON"文字入口
  // 已按需求移除,展开动作由悬浮框行点击触发(渲染层调用 showDebugVariableDetails)
  assert.doesNotMatch(debugInterpreter, /查看完整 JSON/)
  assert.match(debugInterpreter, /window\.expandDebugVariableHover = function/)
  assert.match(debugInterpreter, /window\.hideDebugVariableDetails = function/)
  assert.match(hoverLayout, /window\.showDebugVariableDetails\(/)
  assert.match(debugInterpreter, /className = 'debug-variable-popup'/)
  assert.match(styles, /\.debug-variable-popup\b/)
})

test('visible debug pauses flush buffered target output once', () => {
  const client = read('js/cangjie-debug-client.js')

  assert.match(client, /await this\.flushDebuggeeOutput\(frame\.id\)/)
  assert.match(
    client,
    /async flushDebuggeeOutput\(frameId\)[\s\S]*?print\("", flush: true\)/
  )
  assert.doesNotMatch(client, /findCangjieInputBreakpoints/)
  // 求值期间抑制 stderr 噪音;lldb 找不到 stdx .cjo 的 help 提示始终过滤
  assert.match(
    client,
    /hasPendingEvaluation\s*&&\s*\(category === 'stderr' \|\| \/\^\\s\*unknown error\\s\*\$\/i\.test\(outputText\)\)/
  )
  assert.match(
    client,
    /help: check if the .*\.cjo file of the package exists/
  )
})

test('runtime-only frames never borrow the active user source line', () => {
  const debugInterpreter = read('js/debug-interpreter.js')
  const client = read('js/cangjie-debug-client.js')

  assert.match(
    debugInterpreter,
    /if\s*\(!frame\s*\|\|\s*!frame\.source\s*\|\|\s*!frame\.source\.path\)[\s\S]*?clearDebugLine/
  )
  // 修复前自动 continue 逻辑(已删除):
  //   if (!userFrame && stopBody.reason === 'step') { ... this.request('continue', ...) }
  // 这个逻辑在远程模式下路径映射有缺陷时会无限 continue 直到程序结束。
  // 修复后改为:找不到 userFrame 时退回栈顶帧并停住,不自动 continue。
  assert.doesNotMatch(
    client,
    /if\s*\(!userFrame\s*&&\s*stopBody\.reason\s*===\s*'step'\)[\s\S]*?this\.request\('continue'/
  )
  // 新行为:找不到 userFrame 时 emit output 提示,然后继续走 selectStoppedFrame
  assert.match(
    client,
    /if\s*\(!userFrame\)\s*\{[\s\S]*?emit\('output'/
  )
})

test('debug console preserves unterminated print output beside stdin', () => {
  const debugInterpreter = read('js/debug-interpreter.js')

  assert.match(debugInterpreter, /const endsWithNewline = normalized\.endsWith\('\\n'\)/)
  assert.match(debugInterpreter, /partial:\s*isLast\s*&&\s*!endsWithNewline/)
})