import process from 'node:process'
import { access, mkdir, mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
import { constants as fsConstants } from 'node:fs'
import { spawn } from 'node:child_process'
import net from 'node:net'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { chromium, expect } from '@playwright/test'

const TEXT = {
  accessControl: '\u8bbf\u95ee\u63a7\u5236',
  active: '\u542f\u7528',
  adminBootstrapTitle: '\u7cfb\u7edf\u5c1a\u672a\u521d\u59cb\u5316\u9996\u4e2a\u7ba1\u7406\u5458\u8d26\u53f7',
  adminRoleName: '\u7ba1\u7406\u5458',
  adminBootstrapAction: '\u521d\u59cb\u5316\u7ba1\u7406\u5458',
  adminBootstrapPageTitle: '\u521d\u59cb\u5316\u9996\u4e2a\u7ba1\u7406\u5458\u8d26\u53f7',
  appTitle: '\u7528\u6237\u7ba1\u7406\u7cfb\u7edf',
  assignPermissions: '\u5206\u914d\u6743\u9650',
  assignRoles: '\u5206\u914d\u89d2\u8272',
  assignRolesAction: '\u89d2\u8272',
  auditLogs: '\u5ba1\u8ba1\u65e5\u5fd7',
  backToLogin: '\u8fd4\u56de\u767b\u5f55',
  bootstrapAdminConfirmPasswordPlaceholder: '\u786e\u8ba4\u7ba1\u7406\u5458\u5bc6\u7801',
  bootstrapAdminEmailPlaceholder: '\u7ba1\u7406\u5458\u90ae\u7bb1',
  bootstrapAdminPasswordPlaceholder: '\u7ba1\u7406\u5458\u5bc6\u7801',
  bootstrapAdminSecretPlaceholder: 'Bootstrap Secret',
  bootstrapAdminSubmit: '\u5b8c\u6210\u521d\u59cb\u5316\u5e76\u8fdb\u5165\u7cfb\u7edf',
  bootstrapAdminUsernamePlaceholder: '\u7ba1\u7406\u5458\u7528\u6237\u540d',
  changePassword: '\u4fee\u6539\u5bc6\u7801',
  confirmPasswordPlaceholder: '\u786e\u8ba4\u5bc6\u7801',
  createAccount: '\u521b\u5efa\u8d26\u53f7',
  createUser: '\u521b\u5efa\u7528\u6237',
  createUserEmailPlaceholder: '\u90ae\u7bb1\u5730\u5740',
  createUserPasswordPlaceholder: '\u8bf7\u8f93\u5165\u521d\u59cb\u5bc6\u7801',
  createUserUsernamePlaceholder: '\u8bf7\u8f93\u5165\u7528\u6237\u540d',
  createRole: '\u521b\u5efa\u89d2\u8272',
  createPermission: '\u521b\u5efa\u6743\u9650',
  dashboard: '\u603b\u89c8',
  delete: '\u5220\u9664',
  deleteConfirm: '\u786e\u5b9a\u5220\u9664',
  deviceManagement: '\u8bbe\u5907\u7ba1\u7406',
  devices: '\u8bbe\u5907',
  disabled: '\u7981\u7528',
  edit: '\u7f16\u8f91',
  editUser: '\u7f16\u8f91\u7528\u6237',
  emailCodeLogin: '\u90ae\u7bb1\u9a8c\u8bc1\u7801',
  emailActivationSuccess: '\u90ae\u7bb1\u9a8c\u8bc1\u6210\u529f',
  export: '\u5bfc\u51fa',
  forgotPassword: '\u5fd8\u8bb0\u5bc6\u7801\uff1f',
  integration: '\u96c6\u6210\u80fd\u529b',
  loginAction: '\u767b\u5f55',
  loginLogs: '\u767b\u5f55\u65e5\u5fd7',
  loginNow: '\u7acb\u5373\u767b\u5f55',
  logout: '\u9000\u51fa\u767b\u5f55',
  logoutOthers: '\u9000\u51fa\u5176\u4ed6\u8bbe\u5907',
  name: '\u540d\u79f0',
  newPassword: '\u65b0\u5bc6\u7801',
  newPasswordPlaceholder: '\u8bf7\u8f93\u5165\u65b0\u5bc6\u7801',
  nickname: '\u6635\u79f0',
  oldPassword: '\u5f53\u524d\u5bc6\u7801',
  oldPasswordPlaceholder: '\u8bf7\u8f93\u5165\u5f53\u524d\u5bc6\u7801',
  operationLogs: '\u64cd\u4f5c\u65e5\u5fd7',
  passwordPlaceholder: '\u5bc6\u7801',
  permissions: '\u6743\u9650\u7ba1\u7406',
  permissionsAction: '\u6743\u9650',
  permissionsHint: '\u9009\u62e9\u8981\u5206\u914d\u7ed9\u8be5\u89d2\u8272\u7684\u6743\u9650',
  profile: '\u4e2a\u4eba\u8d44\u6599',
  registerEmailPlaceholder: '\u90ae\u7bb1\u5730\u5740\uff08\u9009\u586b\uff09',
  registerSuccess: '\u6ce8\u518c\u6210\u529f',
  roleFilter: '\u89d2\u8272\u540d\u79f0/\u4ee3\u7801',
  roles: '\u89d2\u8272\u7ba1\u7406',
  save: '\u4fdd\u5b58',
  security: '\u5b89\u5168\u8bbe\u7f6e',
  smsCodeLogin: '\u77ed\u4fe1\u9a8c\u8bc1\u7801',
  status: '\u72b6\u6001',
  systemManagement: '\u7cfb\u7edf\u7ba1\u7406',
  todaySuccessLogins: '\u4eca\u65e5\u6210\u529f\u767b\u5f55',
  totalUsers: '\u7528\u6237\u603b\u6570',
  trust: '\u4fe1\u4efb',
  untrust: '\u53d6\u6d88\u4fe1\u4efb',
  userDetail: '\u7528\u6237\u8be6\u60c5',
  userDetailAction: '\u8be6\u60c5',
  userId: '\u7528\u6237 ID',
  usernamePlaceholder: '\u7528\u6237\u540d',
  users: '\u7528\u6237\u7ba1\u7406',
  usersFilter: '\u7528\u6237\u540d/\u90ae\u7bb1/\u624b\u673a\u53f7',
  webhooks: 'Webhooks',
  welcomeLogin: '\u6b22\u8fce\u767b\u5f55',
}

const BASE_URL = (process.env.E2E_BASE_URL ?? 'http://127.0.0.1:3000').replace(/\/$/, '')
const VIEWPORTS = [
  { name: 'desktop', width: 1440, height: 960 },
  { name: 'tablet', width: 820, height: 1180 },
  { name: 'mobile', width: 390, height: 844 },
]
const IGNORED_CONSOLE_ERRORS = [
  'Static function can not consume context like dynamic theme',
]
const IGNORED_REQUEST_FAILURES = new Set([
  'net::ERR_ABORTED',
  'net::ERR_FAILED',
])
const DEBUG = process.env.E2E_DEBUG === '1'
const STARTUP_TIMEOUT_MS = Number(process.env.E2E_STARTUP_TIMEOUT_MS ?? 30000)
const SMTP_CAPTURE_FILE = (process.env.E2E_SMTP_CAPTURE_FILE ?? '').trim()
const SESSION_PRESENCE_COOKIE_NAME = 'ums_session_present'

let managedCdpUrl = null
const IS_WINDOWS = process.platform === 'win32'

function appUrl(pathname) {
  return new URL(pathname, `${BASE_URL}/`).toString()
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

function escapeRegex(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

function requireEnv(name) {
  const value = (process.env[name] ?? '').trim()
  if (!value) {
    throw new Error(`${name} is required.`)
  }
  return value
}

async function readCapturedMessages() {
  if (!SMTP_CAPTURE_FILE) {
    return []
  }

  try {
    const content = await readFile(SMTP_CAPTURE_FILE, 'utf8')
    return content
      .split(/\r?\n/)
      .filter(Boolean)
      .flatMap((line) => {
        try {
          return [JSON.parse(line)]
        } catch {
          return []
        }
      })
  } catch (error) {
    if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
      return []
    }
    throw error
  }
}

function normalizeEmail(value) {
  return String(value ?? '')
    .trim()
    .replace(/^<|>$/g, '')
    .toLowerCase()
}

function capturedMessageMatchesRecipient(message, email) {
  const target = normalizeEmail(email)
  if (!target) {
    return false
  }

  const recipients = Array.isArray(message?.rcptTo) ? message.rcptTo : []
  return recipients.some((candidate) => normalizeEmail(candidate) === target)
}

function extractActivationLink(message) {
  const body = String(message?.data ?? '')
  const match = body.match(/https?:\/\/[^\s"<]+\/activate-account\?token=[^"\s<]+/)
  return match?.[0] ?? null
}

async function waitForActivationLink(email, timeoutMs = 20_000) {
  const startedAt = Date.now()

  while (Date.now() - startedAt < timeoutMs) {
    const messages = await readCapturedMessages()
    const matchedMessages = messages.filter((message) => capturedMessageMatchesRecipient(message, email))

    for (let index = matchedMessages.length - 1; index >= 0; index -= 1) {
      const activationLink = extractActivationLink(matchedMessages[index])
      if (activationLink) {
        return activationLink
      }
    }

    await delay(250)
  }

  throw new Error(`Timed out waiting for activation email for ${email}.`)
}

async function fetchAuthCapabilitiesSnapshot() {
  const response = await fetch(appUrl('/api/v1/auth/capabilities'))
  if (!response.ok) {
    throw new Error(`Failed to fetch auth capabilities: ${response.status} ${response.statusText}`)
  }

  const payload = await response.json()
  return payload?.data ?? {}
}

function resolveCdpUrl() {
  if (managedCdpUrl) {
    return managedCdpUrl
  }

  const baseUrl = (process.env.E2E_PLAYWRIGHT_CDP_URL ?? process.env.E2E_CDP_BASE_URL ?? '').trim()
  if (baseUrl) {
    return baseUrl
  }

  const port = Number(process.env.E2E_CDP_PORT ?? 0)
  if (port > 0) {
    return `http://127.0.0.1:${port}`
  }

  throw new Error('E2E_PLAYWRIGHT_CDP_URL or E2E_CDP_PORT is required.')
}

function createSignals() {
  return {
    consoleErrors: [],
    dialogs: [],
    pageErrors: [],
    popups: [],
    requestFailures: [],
    unauthorizedResponses: [],
    windowGuardEvents: [],
  }
}

function logDebug(message) {
  if (DEBUG) {
    console.log(`[debug] ${message}`)
  }
}

function formatError(error) {
  if (!error) {
    return 'unknown error'
  }

  if (error instanceof Error) {
    return error.message || error.name
  }

  return String(error)
}

function isRetryableTargetError(error) {
  const message = formatError(error)
  return (
    message.includes('Target page, context or browser has been closed') ||
    message.includes('Target closed') ||
    message.includes('Browser has been closed')
  )
}

async function assertFileExists(filePath) {
  await access(filePath, fsConstants.F_OK)
}

function isHeadlessShellBrowser(browserPath) {
  return path.basename(browserPath).toLowerCase().includes('headless-shell')
}

async function resolveManagedBrowserPath() {
  const envCandidates = [
    process.env.E2E_BROWSER_PATH,
    process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
    process.env.CHROME_HEADLESS_SHELL_PATH,
  ]
    .map((value) => (value ?? '').trim())
    .filter(Boolean)

  for (const candidate of envCandidates) {
    await assertFileExists(candidate)
    return candidate
  }

  const platformCandidates = IS_WINDOWS
    ? [
        'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
        'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
        'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
        'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
      ]
    : [
        '/snap/bin/chromium',
        '/usr/bin/chromium',
        '/usr/bin/chromium-browser',
        '/usr/bin/google-chrome',
        '/usr/bin/google-chrome-stable',
        '/usr/bin/microsoft-edge',
        '/usr/bin/msedge',
      ]

  for (const candidate of platformCandidates) {
    try {
      await assertFileExists(candidate)
      return candidate
    } catch {
      continue
    }
  }

  const baseDir = IS_WINDOWS
    ? path.join(process.env.LOCALAPPDATA ?? '', 'ms-playwright')
    : path.join(process.env.HOME ?? '', '.cache', 'ms-playwright')
  const candidates = []

  try {
    const entries = await readdir(baseDir, { withFileTypes: true })
    for (const entry of entries) {
      if (!entry.isDirectory() || !entry.name.startsWith('chromium_headless_shell-')) {
        continue
      }

      candidates.push(
        path.join(
          baseDir,
          entry.name,
          IS_WINDOWS ? 'chrome-headless-shell-win64' : 'chrome-headless-shell-linux64',
          IS_WINDOWS ? 'chrome-headless-shell.exe' : 'chrome-headless-shell',
        ),
      )
    }
  } catch {
    throw new Error(`failed to scan Playwright browser cache under ${baseDir}`)
  }

  candidates.sort().reverse()
  for (const candidate of candidates) {
    try {
      await assertFileExists(candidate)
      return candidate
    } catch {
      continue
    }
  }

  throw new Error('No compatible browser found for Playwright CDP E2E.')
}

async function createManagedBrowserProfileDir(browserPath, port) {
  if (!isHeadlessShellBrowser(browserPath)) {
    return await mkdtemp(path.join(tmpdir(), 'pw-playwright-cdp-'))
  }

  const profileRoot = path.join(process.cwd(), '.cache', 'cdp-profiles')
  await mkdir(profileRoot, { recursive: true })
  return path.join(profileRoot, `pw-profile-playwright-cdp-${port}`)
}

function startManagedBrowser(browserPath, port, profileDir) {
  const args = [`--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`, '--no-sandbox']

  if (isHeadlessShellBrowser(browserPath)) {
    args.push('--single-process')
  } else {
    args.push(
      '--disable-dev-shm-usage',
      '--disable-background-networking',
      '--disable-background-timer-throttling',
      '--disable-renderer-backgrounding',
      '--disable-sync',
      '--headless=new',
    )
  }

  args.push('about:blank')

  const browserProcess = spawn(browserPath, args, {
    stdio: ['ignore', 'ignore', 'pipe'],
    windowsHide: true,
  })

  let stderr = ''
  browserProcess.stderr?.on('data', (chunk) => {
    stderr += chunk.toString()
    if (stderr.length > 4000) {
      stderr = stderr.slice(-4000)
    }
  })

  browserProcess.on('exit', (code, signal) => {
    if (code !== 0 && signal == null) {
      console.error(`managed browser exited unexpectedly with code ${code}`)
      const details = stderr.trim()
      if (details) {
        console.error(details)
      }
    }
  })

  return browserProcess
}

async function killManagedBrowser(browserProcess) {
  if (!browserProcess || browserProcess.exitCode != null || browserProcess.pid == null) {
    return
  }

  if (!IS_WINDOWS) {
    try {
      browserProcess.kill('SIGKILL')
    } catch {
      // ignore
    }
    return
  }

  await new Promise((resolve) => {
    const killer = spawn('taskkill', ['/PID', String(browserProcess.pid), '/T', '/F'], {
      stdio: 'ignore',
      windowsHide: true,
    })

    killer.once('error', () => {
      try {
        browserProcess.kill('SIGKILL')
      } catch {
        // ignore
      }
      resolve()
    })

    killer.once('exit', () => resolve())
  })
}

async function getFreePort() {
  return await new Promise((resolve, reject) => {
    const server = net.createServer()
    server.unref()
    server.on('error', reject)
    server.listen(0, '127.0.0.1', () => {
      const address = server.address()
      if (address == null || typeof address === 'string') {
        server.close(() => reject(new Error('failed to resolve a free port')))
        return
      }

      server.close((error) => {
        if (error) {
          reject(error)
          return
        }
        resolve(address.port)
      })
    })
  })
}

async function waitForHttp(url, timeoutMs, label) {
  const startedAt = Date.now()
  let lastError = null

  while (Date.now() - startedAt < timeoutMs) {
    try {
      const response = await fetch(url)
      if (response.ok) {
        return response
      }
      lastError = new Error(`${label} returned ${response.status}`)
    } catch (error) {
      lastError = error
    }
    await delay(250)
  }

  throw new Error(`timed out waiting for ${label}: ${formatError(lastError)}`)
}

async function waitForJson(url, timeoutMs, label = url) {
  const response = await waitForHttp(url, timeoutMs, label)
  return await response.json()
}

function isIgnoredConsoleError(text) {
  if (text.includes('favicon') && text.includes('404')) {
    return true
  }

  return IGNORED_CONSOLE_ERRORS.some((value) => text.includes(value))
}

function formatSignals(signals) {
  const lines = []

  if (signals.windowGuardEvents.length > 0) {
    lines.push(`window-guard events:\n${signals.windowGuardEvents.join('\n')}`)
  }
  if (signals.dialogs.length > 0) {
    lines.push(`native dialogs:\n${signals.dialogs.join('\n')}`)
  }
  if (signals.popups.length > 0) {
    lines.push(`popup pages:\n${signals.popups.join('\n')}`)
  }
  if (signals.pageErrors.length > 0) {
    lines.push(`page errors:\n${signals.pageErrors.join('\n\n')}`)
  }
  if (signals.consoleErrors.length > 0) {
    lines.push(`console errors:\n${signals.consoleErrors.join('\n')}`)
  }
  if (signals.requestFailures.length > 0) {
    lines.push(`request failures:\n${signals.requestFailures.join('\n')}`)
  }
  if (signals.unauthorizedResponses.length > 0) {
    lines.push(`unauthorized responses:\n${signals.unauthorizedResponses.join('\n')}`)
  }

  return lines.join('\n\n')
}

function assertCleanSignals(signals) {
  const output = formatSignals(signals)
  if (output) {
    throw new Error(output)
  }
}

function attachSignalCollectors(page, signals) {
  const onConsole = (message) => {
    if (message.type() !== 'error') {
      return
    }

    const text = message.text()
    if (text.startsWith('[window-guard]')) {
      signals.windowGuardEvents.push(text)
      return
    }

    if (!isIgnoredConsoleError(text)) {
      signals.consoleErrors.push(text)
    }
  }

  const onDialog = (dialog) => {
    signals.dialogs.push(`${dialog.type()}: ${dialog.message()}`)
    void dialog.dismiss().catch(() => {})
  }

  const onPageError = (error) => {
    signals.pageErrors.push(error.stack ?? error.message)
  }

  const onPopup = (popup) => {
    signals.popups.push(popup.url() || 'about:blank')
    void popup.close().catch(() => {})
  }

  const onRequestFailed = (request) => {
    const failureText = request.failure()?.errorText ?? 'unknown failure'
    if (!IGNORED_REQUEST_FAILURES.has(failureText)) {
      signals.requestFailures.push(`${request.method()} ${request.url()} :: ${failureText}`)
    }
  }

  const onResponse = (response) => {
    if (response.status() === 401) {
      signals.unauthorizedResponses.push(`${response.request().method()} ${response.url()}`)
    }
  }

  page.on('console', onConsole)
  page.on('dialog', onDialog)
  page.on('pageerror', onPageError)
  page.on('popup', onPopup)
  page.on('requestfailed', onRequestFailed)
  page.on('response', onResponse)

  return () => {
    page.off('console', onConsole)
    page.off('dialog', onDialog)
    page.off('pageerror', onPageError)
    page.off('popup', onPopup)
    page.off('requestfailed', onRequestFailed)
    page.off('response', onResponse)
  }
}

async function assertBaseUrlServesAdminApp(page) {
  await page.goto(appUrl('/login'), { waitUntil: 'domcontentloaded' })
  await page.waitForLoadState('networkidle').catch(() => {})

  const title = await page.title().catch(() => '')
  const bodyText = (await page.locator('body').textContent())?.trim() ?? ''
  const matchesAppTitle = title.includes(TEXT.appTitle)
  const matchesAppBody = bodyText.includes(TEXT.welcomeLogin) || bodyText.includes(TEXT.adminBootstrapTitle)
  if (matchesAppTitle || matchesAppBody) {
    return
  }

  throw new Error(
    `E2E_BASE_URL resolved to ${appUrl('/login')}, but the page does not look like the admin app. ` +
      `title=${JSON.stringify(title)} body_excerpt=${JSON.stringify(bodyText.slice(0, 160))}. ` +
      `Set E2E_BASE_URL to the running frontend app (default expects the Vite dev server on :3000).`,
  )
}

async function resetBrowserState(context, page) {
  logDebug('resetting browser state')
  await page.setViewportSize({ width: VIEWPORTS[0].width, height: VIEWPORTS[0].height })
  await context.clearCookies()
  await page.goto(appUrl('/login'), { waitUntil: 'domcontentloaded' })
  await page.evaluate(() => {
    localStorage.clear()
    sessionStorage.clear()
  })
  await page.goto('about:blank')
}

async function openDevToolsPageTarget() {
  const endpoints = [
    `${resolveCdpUrl()}/json/new?about:blank`,
    `${resolveCdpUrl()}/json/new?url=about:blank`,
  ]

  for (const endpoint of endpoints) {
    for (const method of ['PUT', 'GET']) {
      try {
        await fetch(endpoint, { method })
        return
      } catch {
        // try next variant
      }
    }
  }
}

async function connectBrowserWithRetry() {
  let lastError = null

  for (let attempt = 1; attempt <= 3; attempt += 1) {
    try {
      return await chromium.connectOverCDP(resolveCdpUrl())
    } catch (error) {
      lastError = error
      if (attempt >= 3) {
        break
      }
      await delay(500)
    }
  }

  throw lastError ?? new Error('Failed to connect to the Chromium CDP endpoint.')
}

async function ensurePersistentPage(browser, context) {
  let page = context.pages().find((candidate) => !candidate.isClosed())
  if (page) {
    return page
  }

  try {
    const session = await browser.newBrowserCDPSession()
    try {
      await session.send('Target.createTarget', { url: 'about:blank' })
    } finally {
      await session.detach().catch(() => {})
    }
  } catch {
    // fall through to DevTools HTTP endpoint fallback
  }

  await openDevToolsPageTarget()

  for (let attempt = 0; attempt < 50; attempt += 1) {
    page = context.pages().find((candidate) => !candidate.isClosed())
    if (page) {
      return page
    }
    await delay(100)
  }

  return null
}

async function getProtectedRouteRedirect(page) {
  return await page.evaluate(() => {
    return {
      path: window.location.pathname,
      redirectFrom: history.state?.usr?.from?.pathname ?? null,
      title: document.title,
    }
  })
}

async function clickSidebarMenu(page, label) {
  const menuItems = page
    .locator('.ant-layout-sider .ant-menu-item, .ant-drawer .ant-menu-item')
    .filter({ hasText: label })

  const count = await menuItems.count()
  for (let index = 0; index < count; index += 1) {
    const menuItem = menuItems.nth(index)
    if (await menuItem.isVisible()) {
      await forceClick(menuItem)
      return
    }
  }

  throw new Error(`No visible menu item found for ${label}.`)
}

async function expandSidebarGroup(page, label) {
  const groups = page
    .locator('.ant-layout-sider .ant-menu-submenu-title, .ant-drawer .ant-menu-submenu-title')
    .filter({ hasText: label })

  const count = await groups.count()
  for (let index = 0; index < count; index += 1) {
    const group = groups.nth(index)
    if (await group.isVisible()) {
      await forceClick(group)
      return
    }
  }

  throw new Error(`No visible menu group found for ${label}.`)
}

async function forceFillInput(locator, value) {
  await expect(locator).toBeVisible()
  await locator.evaluate((element, nextValue) => {
    if (!(element instanceof HTMLInputElement)) {
      throw new Error('Target element is not an input.')
    }

    element.focus()
    const prototype = Object.getPrototypeOf(element)
    const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value')
    if (descriptor?.set) {
      descriptor.set.call(element, nextValue)
    } else {
      element.value = nextValue
    }

    element.dispatchEvent(new Event('input', { bubbles: true }))
    element.dispatchEvent(new Event('change', { bubbles: true }))
  }, value)
}

async function forceClick(locator) {
  await expect(locator).toBeVisible()
  await locator.evaluate((element) => {
    if (!(element instanceof HTMLElement)) {
      throw new Error('Target element is not clickable.')
    }

    element.scrollIntoView({ block: 'center', inline: 'center' })
    const target =
      element.closest(
        'button, a, [role="button"], [role="menuitem"], .ant-btn, .ant-menu-item, .ant-modal-close, .ant-drawer-close',
      ) ?? element

    target.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
    target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
    target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
    target.dispatchEvent(new MouseEvent('click', { bubbles: true }))
  })
}

async function hasHttpOnlyRefreshCookie(page) {
  const cookies = await page.context().cookies()
  return cookies.some((cookie) => cookie.name === 'ums_refresh_token' && Boolean(cookie.value))
}

async function readSessionPresenceCookie(page) {
  return await page.evaluate((cookieName) => {
    const target = `${cookieName}=`
    const matched = document.cookie
      .split(';')
      .map((cookie) => cookie.trim())
      .find((cookie) => cookie.startsWith(target))

    return matched ? matched.slice(target.length) : null
  }, SESSION_PRESENCE_COOKIE_NAME)
}

async function assertApiSuccessResponse(response, label) {
  const responseBody = await response.text().catch(() => '')
  if (!response.ok()) {
    throw new Error(`${label} request failed: ${response.status()} ${responseBody}`)
  }

  let payload
  try {
    payload = JSON.parse(responseBody)
  } catch (error) {
    throw new Error(`${label} response is not valid JSON: ${responseBody}`)
  }

  if (payload?.code !== 0) {
    throw new Error(`${label} response code ${payload?.code}: ${payload?.message ?? responseBody}`)
  }

  return payload
}

async function waitForSessionCookies(context, timeoutMs = 10_000) {
  const startedAt = Date.now()
  while (Date.now() - startedAt < timeoutMs) {
    const cookies = await context.cookies()
    const hasRefresh = cookies.some((cookie) => cookie.name === 'ums_refresh_token' && cookie.value)
    const hasPresence = cookies.some((cookie) => cookie.name === 'ums_session_present' && cookie.value === '1')
    if (hasRefresh && hasPresence) {
      return
    }
    await delay(100)
  }

  throw new Error('session cookies were not persisted after login within timeout')
}

async function loginWithPassword(page, username, password, expectedUrlPattern) {
  const usernameInput = page
    .locator(`input[autocomplete="username"], input[placeholder="${TEXT.usernamePlaceholder}"]`)
    .first()
  const loginForm = usernameInput.locator('xpath=ancestor::form[1]')

  await forceFillInput(usernameInput, username)
  await forceFillInput(loginForm.locator('input[type="password"]').first(), password)
  const loginResponsePromise = page.waitForResponse((response) => {
      return response.url().includes('/api/v1/auth/login') && response.request().method() === 'POST'
    }, { timeout: 5_000 }).catch(() => null)
  await forceClick(loginForm.locator('button[type="submit"]').first())

  const loginResponse = await loginResponsePromise
  if (loginResponse) {
    await assertApiSuccessResponse(loginResponse, 'password login')
  }
  await waitForSessionCookies(page.context())

  if (expectedUrlPattern) {
    await expect(page).toHaveURL(expectedUrlPattern, { timeout: 30 * 1000 })
  }
}

async function expectLoggedInLanding(page, timeoutMs = 30 * 1000) {
  await expect(page).toHaveURL(/\/(dashboard|profile)$/, { timeout: timeoutMs })

  const currentUrl = page.url()
  if (currentUrl.endsWith('/dashboard')) {
    await expect(page.getByText(TEXT.todaySuccessLogins)).toBeVisible()
    return
  }

  await expect(page.locator('body')).toContainText(TEXT.profile)
}

async function loginFromLoginPage(page) {
  const username = requireEnv('E2E_LOGIN_USERNAME')
  const password = requireEnv('E2E_LOGIN_PASSWORD')

  await page.goto(appUrl('/login'))
  await expect(page).toHaveURL(/\/login$/)
  await expect(page.getByRole('heading', { name: TEXT.welcomeLogin })).toBeVisible()

  await loginWithPassword(page, username, password)
  await expectLoggedInLanding(page)

  return { username, password }
}

async function verifyAdminBootstrapWorkflow(page) {
  const username = requireEnv('E2E_LOGIN_USERNAME')
  const password = requireEnv('E2E_LOGIN_PASSWORD')
  const email = (process.env.E2E_LOGIN_EMAIL ?? `${username}@example.com`).trim()
  const bootstrapSecret = (process.env.E2E_BOOTSTRAP_SECRET ?? process.env.BOOTSTRAP_SECRET ?? '').trim()
  if (!bootstrapSecret) {
    throw new Error('E2E_BOOTSTRAP_SECRET or BOOTSTRAP_SECRET is required when E2E_EXPECT_ADMIN_BOOTSTRAP=1.')
  }

  const capabilitiesResponse = page.waitForResponse((response) => {
    return response.url().includes('/api/v1/auth/capabilities') && response.request().method() === 'GET'
  })

  await page.goto(appUrl('/login'))
  const capabilitiesPayload = await (await capabilitiesResponse).json()
  expect(Boolean(capabilitiesPayload?.data?.admin_bootstrap_required)).toBe(true)

  await expect(page.getByText(TEXT.adminBootstrapTitle)).toBeVisible()
  await forceClick(page.getByRole('button', { name: TEXT.adminBootstrapAction }))
  await expect(page).toHaveURL(/\/bootstrap-admin$/)
  await expect(page.getByRole('heading', { name: TEXT.adminBootstrapPageTitle })).toBeVisible()

  await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminUsernamePlaceholder}"]`).first(), username)
  await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminEmailPlaceholder}"]`).first(), email)
  await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminSecretPlaceholder}"]`).first(), bootstrapSecret)
  await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminPasswordPlaceholder}"]`).first(), password)
  await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminConfirmPasswordPlaceholder}"]`).first(), password)

  const [bootstrapResponse] = await Promise.all([
    page.waitForResponse((response) => {
      return response.url().includes('/api/v1/auth/bootstrap-admin') && response.request().method() === 'POST'
    }),
    forceClick(page.getByRole('button', { name: TEXT.bootstrapAdminSubmit })),
  ])
  await assertApiSuccessResponse(bootstrapResponse, 'bootstrap admin')

  await expectLoggedInLanding(page)

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
  await expect(page.getByText(TEXT.adminBootstrapTitle)).toHaveCount(0)
}

async function verifyPublicRegistration(page) {
  const username = `e2e_register_${Date.now()}`
  const password = 'Register123!@#'

  await page.goto(appUrl('/login'))
  await expect(page.getByRole('link', { name: TEXT.createAccount })).toBeVisible()
  await forceClick(page.getByRole('link', { name: TEXT.createAccount }))
  await expect(page).toHaveURL(/\/register$/)
  await expect(page.getByRole('heading', { name: TEXT.createAccount })).toBeVisible()

  await forceFillInput(page.getByPlaceholder(TEXT.usernamePlaceholder), username)
  await forceFillInput(page.locator(`input[placeholder="${TEXT.passwordPlaceholder}"]`).first(), password)
  await forceFillInput(
    page.locator(`input[placeholder="${TEXT.confirmPasswordPlaceholder}"]`).first(),
    password,
  )
  const [registerResponse] = await Promise.all([
    page.waitForResponse((response) => {
      return response.url().includes('/api/v1/auth/register') && response.request().method() === 'POST'
    }),
    forceClick(page.getByRole('button', { name: TEXT.createAccount })),
  ])
  await assertApiSuccessResponse(registerResponse, 'register')

  await expect(page.locator('.ant-result-title').filter({ hasText: TEXT.registerSuccess }).first()).toBeVisible({ timeout: 20 * 1000 })
  await forceClick(page.getByRole('button', { name: TEXT.backToLogin }))
  await expect(page).toHaveURL(/\/login$/)

  await loginWithPassword(page, username, password, /\/profile$/)
  await expect(page.locator('body')).toContainText(TEXT.profile)

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyEmailActivationWorkflow(page) {
  const username = `e2e_activate_${Date.now()}`
  const email = `${username}@example.com`
  const password = 'Register123!@#'

  await page.goto(appUrl('/register'))
  await expect(page).toHaveURL(/\/register$/)
  await expect(page.getByRole('heading', { name: TEXT.createAccount })).toBeVisible()

  await forceFillInput(page.getByPlaceholder(TEXT.usernamePlaceholder), username)
  await forceFillInput(page.getByPlaceholder(TEXT.registerEmailPlaceholder), email)
  await forceFillInput(page.locator(`input[placeholder="${TEXT.passwordPlaceholder}"]`).first(), password)
  await forceFillInput(
    page.locator(`input[placeholder="${TEXT.confirmPasswordPlaceholder}"]`).first(),
    password,
  )

  const [registerResponse] = await Promise.all([
    page.waitForResponse((response) => {
      return response.url().includes('/api/v1/auth/register') && response.request().method() === 'POST'
    }),
    forceClick(page.getByRole('button', { name: TEXT.createAccount })),
  ])
  await assertApiSuccessResponse(registerResponse, 'register email activation')
  await expect(page.locator('.ant-result-title').filter({ hasText: TEXT.registerSuccess }).first()).toBeVisible({ timeout: 20 * 1000 })

  const activationLink = await waitForActivationLink(email)
  const [activationResponse] = await Promise.all([
    page.waitForResponse((response) => {
      return response.url().includes('/api/v1/auth/activate') && response.request().method() === 'GET'
    }),
    page.goto(activationLink),
  ])
  await assertApiSuccessResponse(activationResponse, 'activate email')
  await expect(page.locator('body')).toContainText(TEXT.emailActivationSuccess, { timeout: 20 * 1000 })
  await forceClick(page.getByRole('button', { name: TEXT.loginNow }))
  await expect(page).toHaveURL(/\/login$/)

  await loginWithPassword(page, username, password, /\/profile$/)
  await expect(page.locator('body')).toContainText(TEXT.profile)

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function runScenario(browser, context, name, fn) {
  console.log(`START ${name}`)
  let lastError = null

  for (let attempt = 1; attempt <= 2; attempt += 1) {
    const activeContext = browser.contexts()[0] ?? context
    const page = await ensurePersistentPage(browser, activeContext)
    if (!page) {
      throw new Error('No persistent page is available in the Chromium CDP context.')
    }

    for (const extraPage of activeContext.pages()) {
      if (extraPage === page) {
        continue
      }
      await extraPage.close({ runBeforeUnload: false }).catch(() => {})
    }

    const signals = createSignals()
    const detachSignals = attachSignalCollectors(page, signals)
    const startedAt = Date.now()

    try {
      console.log(`STEP ${name} reset-state`)
      await resetBrowserState(activeContext, page)
      console.log(`STEP ${name} execute`)
      await fn(page)
      assertCleanSignals(signals)
      console.log(`PASS ${name} (${Date.now() - startedAt}ms)`)
      return
    } catch (error) {
      lastError = error
      const signalOutput = formatSignals(signals)
      if (signalOutput) {
        console.error(`SIGNALS ${name}\n${signalOutput}`)
      }

      if (attempt >= 2 || !isRetryableTargetError(error)) {
        throw error
      }

      console.warn(`RETRY ${name} attempt ${attempt + 1}: ${formatError(error)}`)
      await delay(500)
    } finally {
      detachSignals()
      if (!page.isClosed()) {
        await page.goto('about:blank').catch(() => {})
      }
    }
  }

  throw lastError ?? new Error(`Scenario ${name} failed`)
}

async function verifyLoginSurface(page) {
  console.log('STEP login-surface wait-capabilities')
  const capabilitiesResponse = page.waitForResponse((response) => {
    return response.url().includes('/api/v1/auth/capabilities') && response.request().method() === 'GET'
  })

  console.log('STEP login-surface goto-login')
  await page.goto(appUrl('/login'))
  console.log('STEP login-surface capabilities-response')
  const capabilitiesPayload = await (await capabilitiesResponse).json()
  const capabilities = capabilitiesPayload?.data ?? {}

  await expect(page).toHaveTitle(new RegExp(TEXT.appTitle))
  await expect(page.locator('html')).toHaveAttribute('lang', 'zh-CN')
  await expect(page.getByRole('heading', { name: TEXT.welcomeLogin })).toBeVisible()
  await expect(page.getByPlaceholder(TEXT.usernamePlaceholder)).toBeVisible()
  await expect(page.getByPlaceholder(TEXT.passwordPlaceholder)).toBeVisible()

  if (capabilities.email_code) {
    await expect(page.getByRole('tab', { name: TEXT.emailCodeLogin })).toBeVisible()
  } else {
    await expect(page.getByRole('tab', { name: TEXT.emailCodeLogin })).toHaveCount(0)
  }

  if (capabilities.sms_code) {
    await expect(page.getByRole('tab', { name: TEXT.smsCodeLogin })).toBeVisible()
  } else {
    await expect(page.getByRole('tab', { name: TEXT.smsCodeLogin })).toHaveCount(0)
  }

  if (capabilities.password_reset) {
    await expect(page.getByRole('link', { name: TEXT.forgotPassword })).toBeVisible()
  } else {
    await expect(page.getByRole('link', { name: TEXT.forgotPassword })).toHaveCount(0)
  }

  await page.goto(appUrl('/dashboard'))
  await expect(page).toHaveURL(/\/login$/, { timeout: 10 * 1000 })
  const dashboardRedirect = await getProtectedRouteRedirect(page)
  expect(dashboardRedirect.path).toBe('/login')
  expect(dashboardRedirect.redirectFrom).toBe('/dashboard')

  await page.goto(appUrl('/users'))
  await expect(page).toHaveURL(/\/login$/, { timeout: 10 * 1000 })
  const usersRedirect = await getProtectedRouteRedirect(page)
  expect(usersRedirect.path).toBe('/login')
  expect(usersRedirect.redirectFrom).toBe('/users')
}

async function verifyAuthWorkflow(page) {
  logDebug('verifyAuthWorkflow: login /login')
  const credentials = await loginFromLoginPage(page)
  const createdUsername = `e2e_user_${Date.now()}`
  await page.goto(appUrl('/users'))
  await expect(page).toHaveURL(/\/users$/)

  expect(await hasHttpOnlyRefreshCookie(page)).toBe(true)
  expect(await readSessionPresenceCookie(page)).toBe('1')

  const userRow = page.locator('tbody tr').filter({ hasText: credentials.username }).first()
  await expect(userRow).toBeVisible({ timeout: 20 * 1000 })
  await expect(page.getByPlaceholder(TEXT.usersFilter)).toBeVisible()

  await forceClick(userRow.getByRole('button', { name: TEXT.userDetailAction }))
  const userDetailTitle = page.locator('.ant-drawer-title')
  await expect(userDetailTitle).toHaveText(TEXT.userDetail)
  await expect(page.locator('.ant-drawer')).toContainText(TEXT.userId)
  await expect(page.locator('.ant-drawer')).toContainText(credentials.username)
  await page.goto(appUrl('/users'))
  await expect(page.locator('tbody tr').filter({ hasText: credentials.username }).first()).toBeVisible({ timeout: 20 * 1000 })

  await forceClick(userRow.getByRole('button', { name: TEXT.assignRolesAction }))
  const assignRolesTitle = page.locator('.ant-modal-title')
  await expect(assignRolesTitle).toContainText(TEXT.assignRoles)
  await expect(page.locator('.ant-modal')).toContainText(credentials.username)
  await page.goto(appUrl('/users'))
  await expect(page.locator('tbody tr').filter({ hasText: credentials.username }).first()).toBeVisible({ timeout: 20 * 1000 })

  await forceClick(page.getByRole('button', { name: TEXT.createUser }).first())
  await expect(page.locator('.ant-modal-title')).toContainText(TEXT.createUser)
  const createUserModal = page.locator('.ant-modal').last()
  const createUserResponsePromise = page.waitForResponse((response) => {
    return response.url().includes('/api/v1/users') && response.request().method() === 'POST'
  })
  await forceFillInput(
    createUserModal.locator(`input[placeholder="${TEXT.createUserUsernamePlaceholder}"]`).first(),
    createdUsername,
  )
  await forceFillInput(
    createUserModal.locator(`input[placeholder="${TEXT.createUserPasswordPlaceholder}"]`).first(),
    'Pass123!@#',
  )
  await forceFillInput(
    createUserModal.locator(`input[placeholder="${TEXT.createUserEmailPlaceholder}"]`).first(),
    `${createdUsername}@example.com`,
  )
  await forceClick(createUserModal.locator('.ant-btn-primary').last())
  const createUserResponse = await createUserResponsePromise
  await assertApiSuccessResponse(createUserResponse, 'create user')
  await expect(createUserModal).toHaveClass(/ant-zoom-leave/, { timeout: 20 * 1000 })
  await page.goto(appUrl('/users'))
  await expect(page).toHaveURL(/\/users$/)
  await forceFillInput(page.getByPlaceholder(TEXT.usersFilter), createdUsername)
  await expect(page.locator('tbody tr').filter({ hasText: createdUsername }).first()).toBeVisible({ timeout: 20 * 1000 })

  await page.goto(appUrl('/roles'))
  await expect(page).toHaveURL(/\/roles$/)
  await expect(page.getByPlaceholder(TEXT.roleFilter)).toBeVisible()
  await expect(page.getByRole('button', { name: TEXT.createRole })).toBeVisible()

  const adminRoleRow = page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()
  await expect(adminRoleRow).toBeVisible({ timeout: 20 * 1000 })
  await forceClick(adminRoleRow.getByRole('button', { name: TEXT.permissionsAction }))

  const assignPermissionsTitle = page.locator('.ant-modal-title')
  await expect(assignPermissionsTitle).toContainText(TEXT.assignPermissions)
  await expect(page.locator('.ant-modal')).toContainText(TEXT.adminRoleName)
  await expect(page.locator('.ant-modal')).toContainText(TEXT.permissionsHint)
  await page.goto(appUrl('/roles'))
  await expect(page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()).toBeVisible({ timeout: 20 * 1000 })

  await page.goto(appUrl('/dashboard'))
  await expect(page).toHaveURL(/\/dashboard$/)
  await expect(page.getByText(TEXT.todaySuccessLogins)).toBeVisible()
  await expect(page.getByText(TEXT.totalUsers)).toBeVisible()

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
  await expect(await hasHttpOnlyRefreshCookie(page)).toBe(false)
  await expect(await readSessionPresenceCookie(page)).toBeNull()

  await page.goto(appUrl('/dashboard'))
  const postLogoutRedirect = await getProtectedRouteRedirect(page)
  expect(postLogoutRedirect.path).toBe('/login')
  expect(postLogoutRedirect.redirectFrom).toBe('/dashboard')
}

async function verifyResponsiveLogin(page) {
  for (const viewport of VIEWPORTS) {
    logDebug(`verifyResponsiveLogin: ${viewport.name}`)
    await page.setViewportSize({ width: viewport.width, height: viewport.height })
    await page.goto(appUrl('/login'))
    await expect(page).toHaveTitle(new RegExp(TEXT.appTitle))
    await expect(page.getByRole('heading', { name: TEXT.welcomeLogin })).toBeVisible()

    const metrics = await page.evaluate(() => {
      return {
        innerWidth: window.innerWidth,
        bodyScrollWidth: document.body.scrollWidth,
        documentScrollWidth: document.documentElement.scrollWidth,
      }
    })

    expect(metrics.bodyScrollWidth).toBeLessThanOrEqual(metrics.innerWidth + 24)
    expect(metrics.documentScrollWidth).toBeLessThanOrEqual(metrics.innerWidth + 24)
  }
}

async function verifyDesktopAndMobileNavigation(page) {
  logDebug('verifyDesktopAndMobileNavigation: login /login')
  const credentials = requireEnv('E2E_LOGIN_USERNAME')

  await page.setViewportSize({ width: 1440, height: 960 })
  await loginFromLoginPage(page)
  await expect(page.getByText(TEXT.todaySuccessLogins)).toBeVisible()

  await expandSidebarGroup(page, TEXT.accessControl)
  await clickSidebarMenu(page, TEXT.users)
  await expect(page).toHaveURL(/\/users$/)
  await expect(page.locator('tbody tr').filter({ hasText: credentials }).first()).toBeVisible({ timeout: 20 * 1000 })

  await expandSidebarGroup(page, TEXT.accessControl)
  await clickSidebarMenu(page, TEXT.roles)
  await expect(page).toHaveURL(/\/roles$/)
  await expect(page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()).toBeVisible({ timeout: 20 * 1000 })

  await page.setViewportSize({ width: 390, height: 844 })
  await expect
    .poll(async () => await page.evaluate(() => window.innerWidth < 768))
    .toBe(true)
  await page.evaluate(() => window.dispatchEvent(new Event('resize')))
  await expect
    .poll(async () => await page.locator('.ant-layout-header .ant-btn').count())
    .toBeGreaterThan(0)

  const mobileMenuButton = page.locator('.ant-layout-header .ant-btn').first()
  await expect(mobileMenuButton).toBeVisible()
  await forceClick(mobileMenuButton)

  await expect(page.locator('.ant-drawer-content')).toBeVisible({ timeout: 10 * 1000 })
  const mobileDashboardItem = page.locator('.ant-drawer .ant-menu-item').filter({ hasText: TEXT.dashboard }).first()
  await expect(mobileDashboardItem).toBeVisible()
  await forceClick(mobileDashboardItem)
  await expect(page).toHaveURL(/\/dashboard$/)
  await page.goto(appUrl('/dashboard'))
  await expect(page.locator('body')).toContainText(TEXT.todaySuccessLogins, { timeout: 10 * 1000 })
}

async function verifyUserManagementCRUD(page) {
  logDebug('verifyUserManagementCRUD: login /login')
  await loginFromLoginPage(page)

  await expandSidebarGroup(page, TEXT.accessControl)
  await clickSidebarMenu(page, TEXT.users)
  await expect(page).toHaveURL(/\/users$/)

  const testUsername = `e2e_crud_${Date.now()}`
  const testEmail = `${testUsername}@example.com`

  const createUserModal = page.locator('.ant-modal').last()
  await forceClick(page.getByRole('button', { name: TEXT.createUser }).first())
  await expect(createUserModal).toBeVisible({ timeout: 10 * 1000 })

  const createUserResponsePromise = page.waitForResponse((response) => {
    return response.url().includes('/api/v1/users') && response.request().method() === 'POST'
  })
  await forceFillInput(
    createUserModal.locator(`input[placeholder="${TEXT.createUserUsernamePlaceholder}"]`).first(),
    testUsername,
  )
  await forceFillInput(
    createUserModal.locator(`input[placeholder="${TEXT.createUserPasswordPlaceholder}"]`).first(),
    'Crud123!@#',
  )
  await forceFillInput(
    createUserModal.locator(`input[placeholder="${TEXT.createUserEmailPlaceholder}"]`).first(),
    testEmail,
  )
  await forceClick(createUserModal.locator('.ant-btn-primary').last())
  const createUserResponse = await createUserResponsePromise
  await assertApiSuccessResponse(createUserResponse, 'create user CRUD')

  await expect(page.locator('tbody tr').filter({ hasText: testUsername }).first()).toBeVisible({ timeout: 20 * 1000 })

  const userRow = page.locator('tbody tr').filter({ hasText: testUsername }).first()
  await forceClick(userRow.getByRole('button', { name: TEXT.edit }))
  const editDrawer = page.locator('.ant-drawer.ant-drawer-open')
  await expect(editDrawer).toBeVisible({ timeout: 10 * 1000 })

  const editResponsePromise = page.waitForResponse((response) => {
    return response.url().includes(`/api/v1/users/`) && response.request().method() === 'PUT'
  })
  await forceClick(editDrawer.locator('.ant-btn-primary').last())
  const editResponse = await editResponsePromise
  await assertApiSuccessResponse(editResponse, 'edit user CRUD')

  await forceClick(userRow.getByRole('button', { name: TEXT.userDetailAction }))
  const detailDrawer = page.locator('.ant-drawer.ant-drawer-open')
  await expect(detailDrawer).toBeVisible({ timeout: 10 * 1000 })
  await expect(detailDrawer).toContainText(testUsername)

  await page.goto(appUrl('/users'))
  await forceFillInput(page.getByPlaceholder(TEXT.usersFilter), testUsername)
  await expect(page.locator('tbody tr').filter({ hasText: testUsername }).first()).toBeVisible({ timeout: 10 * 1000 })

  await forceClick(userRow.getByRole('button', { name: TEXT.delete }))
  const deleteConfirmModal = page.locator('.ant-popover').filter({ hasText: '确定要删除用户' }).last()
  await expect(deleteConfirmModal).toBeVisible({ timeout: 10 * 1000 })
  const [deleteResponse] = await Promise.all([
    page.waitForResponse((response) => {
      return response.url().includes(`/api/v1/users/`) && response.request().method() === 'DELETE'
    }),
    forceClick(deleteConfirmModal.locator('.ant-popconfirm-buttons .ant-btn-primary').last()),
  ])
  await assertApiSuccessResponse(deleteResponse, 'delete user CRUD')

  await expect(page.locator('tbody tr').filter({ hasText: testUsername }).first()).toHaveCount(0, { timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyRoleManagementCRUD(page) {
  logDebug('verifyRoleManagementCRUD: login /login')
  await loginFromLoginPage(page)

  await expandSidebarGroup(page, TEXT.accessControl)
  await clickSidebarMenu(page, TEXT.roles)
  await expect(page).toHaveURL(/\/roles$/)

  await expect(page.getByRole('button', { name: TEXT.createRole })).toBeVisible()
  await expect(page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()).toBeVisible({ timeout: 10 * 1000 })

  const adminRoleRow = page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()
  await forceClick(adminRoleRow.getByRole('button', { name: TEXT.permissionsAction }))
  const permissionsModal = page.locator('.ant-modal')
  await expect(permissionsModal.locator('.ant-modal-title')).toContainText(TEXT.assignPermissions)

  await forceClick(permissionsModal.locator('.ant-modal-close'))
  await expect(permissionsModal).not.toBeVisible({ timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyDeviceManagement(page) {
  logDebug('verifyDeviceManagement: login /login')
  await loginFromLoginPage(page)

  await page.goto(appUrl('/devices'))
  await expect(page).toHaveURL(/\/devices$/)

  await expect(page.getByText(TEXT.deviceManagement)).toBeVisible({ timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyLoginLogs(page) {
  logDebug('verifyLoginLogs: login /login')
  await loginFromLoginPage(page)

  await expandSidebarGroup(page, TEXT.auditLogs)
  await clickSidebarMenu(page, TEXT.loginLogs)
  await expect(page).toHaveURL(/\/logs\/login$/)

  await expect(page.getByRole('heading', { name: TEXT.loginLogs })).toBeVisible({ timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyOperationLogs(page) {
  logDebug('verifyOperationLogs: login /login')
  await loginFromLoginPage(page)

  await expandSidebarGroup(page, TEXT.auditLogs)
  await clickSidebarMenu(page, TEXT.operationLogs)
  await expect(page).toHaveURL(/\/logs\/operation$/)

  await expect(page.getByRole('heading', { name: TEXT.operationLogs })).toBeVisible({ timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyWebhookManagement(page) {
  logDebug('verifyWebhookManagement: login /login')
  await loginFromLoginPage(page)

  await expandSidebarGroup(page, TEXT.integration)
  await clickSidebarMenu(page, TEXT.webhooks)
  await expect(page).toHaveURL(/\/webhooks$/)

  await expect(page.locator('body')).toContainText('Webhook 管理', { timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyProfileAndSecurity(page) {
  logDebug('verifyProfileAndSecurity: login /login')
  const credentials = await loginFromLoginPage(page)

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.profile, { exact: true }))
  await expect(page).toHaveURL(/\/profile$/)

  await expect(page.locator('body')).toContainText(credentials.username, { timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.locator('.ant-dropdown').getByText(TEXT.security, { exact: true }).last())
  await expect(page).toHaveURL(/\/profile\/security$/)

  await expect(page.getByRole('button', { name: TEXT.changePassword })).toBeVisible({ timeout: 10 * 1000 })

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function verifyDashboardStats(page) {
  logDebug('verifyDashboardStats: login /login')
  await loginFromLoginPage(page)

  await expect(page).toHaveURL(/\/dashboard$/)
  await expect(page.getByText(TEXT.todaySuccessLogins)).toBeVisible({ timeout: 10 * 1000 })
  await expect(page.getByText(TEXT.totalUsers)).toBeVisible()

  await forceClick(page.locator('[class*="userTrigger"]'))
  await forceClick(page.getByText(TEXT.logout, { exact: true }))
  await expect(page).toHaveURL(/\/login$/)
}

async function main() {
  let browser = null
  let managedBrowser = null
  let managedProfileDir = null

  if (process.env.E2E_MANAGED_BROWSER === '1') {
    const browserPath = await resolveManagedBrowserPath()
    const port = await getFreePort()
    managedProfileDir = await createManagedBrowserProfileDir(browserPath, port)
    managedBrowser = startManagedBrowser(browserPath, port, managedProfileDir)
    managedCdpUrl = `http://127.0.0.1:${port}`
    console.log(`LAUNCH playwright-cdp ${browserPath}`)
    await waitForJson(`${managedCdpUrl}/json/version`, STARTUP_TIMEOUT_MS, 'managed browser CDP endpoint')
  }

  console.log('CONNECT playwright-cdp')
  browser = await connectBrowserWithRetry()

  try {
    console.log('CONNECTED playwright-cdp')
    const context = browser.contexts()[0]
    if (!context) {
      throw new Error('No persistent Chromium context is available through CDP.')
    }

    const preflightPage = await ensurePersistentPage(browser, context)
    if (!preflightPage) {
      throw new Error('No persistent page is available in the Chromium CDP context.')
    }
    await assertBaseUrlServesAdminApp(preflightPage)
    const authCapabilities = await fetchAuthCapabilitiesSnapshot()

    if (process.env.E2E_EXPECT_ADMIN_BOOTSTRAP === '1') {
      await runScenario(browser, context, 'admin-bootstrap', verifyAdminBootstrapWorkflow)
    }
    await runScenario(browser, context, 'public-registration', verifyPublicRegistration)
    if (authCapabilities.email_activation) {
      await runScenario(browser, context, 'email-activation', verifyEmailActivationWorkflow)
    } else {
      console.log('SKIP email-activation (auth capability disabled)')
    }
    await runScenario(browser, context, 'login-surface', verifyLoginSurface)
    await runScenario(browser, context, 'auth-workflow', verifyAuthWorkflow)
    await runScenario(browser, context, 'responsive-login', verifyResponsiveLogin)
    await runScenario(browser, context, 'desktop-mobile-navigation', verifyDesktopAndMobileNavigation)
    await runScenario(browser, context, 'user-management-crud', verifyUserManagementCRUD)
    await runScenario(browser, context, 'role-management-crud', verifyRoleManagementCRUD)
    await runScenario(browser, context, 'device-management', verifyDeviceManagement)
    await runScenario(browser, context, 'login-logs', verifyLoginLogs)
    await runScenario(browser, context, 'operation-logs', verifyOperationLogs)
    await runScenario(browser, context, 'webhook-management', verifyWebhookManagement)
    await runScenario(browser, context, 'profile-and-security', verifyProfileAndSecurity)
    await runScenario(browser, context, 'dashboard-stats', verifyDashboardStats)
    console.log('Playwright CDP E2E completed successfully')
  } finally {
    await browser?.close().catch(() => {})
    await killManagedBrowser(managedBrowser)
    if (managedProfileDir) {
      await rm(managedProfileDir, { recursive: true, force: true }).catch(() => {})
    }
    managedCdpUrl = null
  }
}

await main().catch((error) => {
  console.error(error && error.stack ? error.stack : error)
  process.exitCode = 1
})
