import test from 'node:test'
import assert from 'node:assert/strict'

import {
  normalizeLoginType,
  parseViewportSpec,
} from './session-browser-config.js'
import { normalizeTencentCookieMap } from './session-cookie-map.js'
import {
  createEmptyActivityInfo,
  ensureActivityInfoReady,
  ensureLoggedInPresentation,
  extractActivityInfo,
} from './session-activity.js'
import {
  captureQrImageWithRetry,
  ensureLoginTab,
} from './session-page.js'
import { buildArtifactsPayload, buildRedeemPayload } from './session-payload.js'
import { ensureReviewScreenshot } from './session-review.js'
import { resolveInitialQrSessionState } from './session-state.js'
import {
  REDEEM_SUCCESS_AUTO_CLOSE_MS,
  resolveTencentSessionExpiresAt,
} from './session.js'

test('resolveTencentSessionExpiresAt uses normal ttl when no auto-close is armed', () => {
  const nowMs = Date.UTC(2026, 3, 14, 9, 30, 0)
  assert.equal(
    resolveTencentSessionExpiresAt({ autoCloseAt: 0 }, nowMs),
    nowMs + (30 * 60 * 1000),
  )
})

test('resolveTencentSessionExpiresAt keeps redeemed auto-close deadline once armed', () => {
  const nowMs = Date.UTC(2026, 3, 14, 9, 30, 0)
  const autoCloseAt = nowMs + REDEEM_SUCCESS_AUTO_CLOSE_MS

  assert.equal(
    resolveTencentSessionExpiresAt({ autoCloseAt }, nowMs + 60_000),
    autoCloseAt,
  )
})

test('normalizeLoginType collapses aliases and falls back to qq', () => {
  assert.equal(normalizeLoginType(' qc '), 'qq')
  assert.equal(normalizeLoginType('wechat'), 'wx')
  assert.equal(normalizeLoginType('unknown'), 'qq')
  assert.equal(normalizeLoginType(''), 'qq')
})

test('parseViewportSpec accepts valid viewport strings and rejects invalid values', () => {
  assert.deepEqual(parseViewportSpec('1920x1080'), { width: 1920, height: 1080 })
  assert.deepEqual(parseViewportSpec('1920x1080x24'), { width: 1920, height: 1080 })
  assert.equal(parseViewportSpec('799x600'), null)
  assert.equal(parseViewportSpec('broken'), null)
})

test('buildRedeemPayload strips internal paths unless explicitly requested', () => {
  const redeem = {
    code: 'ABC123',
    area: '微信区',
    proofMode: 'basic',
    attempts: [
      {
        attempt: 1,
        redeem: { success: false, message: '验证码错误' },
      },
    ],
    final: {
      attempt: 2,
      redeem: { success: true, message: '兑换成功' },
    },
    finishedAt: '2026-04-14T09:40:00.000Z',
    screenshotPath: '/tmp/redeem.png',
    htmlPath: '/tmp/redeem.html',
    resultPath: '/tmp/redeem.json',
  }

  assert.deepEqual(buildRedeemPayload(redeem), {
    code: 'ABC123',
    area: '微信区',
    proofMode: 'basic',
    attempts: [
      {
        attempt: 1,
        redeem: { success: false, message: '验证码错误' },
      },
    ],
    final: {
      attempt: 2,
      redeem: { success: true, message: '兑换成功' },
    },
    finishedAt: '2026-04-14T09:40:00.000Z',
  })

  assert.deepEqual(buildRedeemPayload(redeem, { includeInternalPaths: true }), {
    code: 'ABC123',
    area: '微信区',
    proofMode: 'basic',
    attempts: [
      {
        attempt: 1,
        redeem: { success: false, message: '验证码错误' },
      },
    ],
    final: {
      attempt: 2,
      redeem: { success: true, message: '兑换成功' },
    },
    finishedAt: '2026-04-14T09:40:00.000Z',
    screenshotPath: '/tmp/redeem.png',
    htmlPath: '/tmp/redeem.html',
    resultPath: '/tmp/redeem.json',
  })
})

test('buildArtifactsPayload only exposes internal artifact paths in debug mode', () => {
  const session = {
    sessionDir: '/tmp/session',
    qrImagePath: '/tmp/session/qq-qr.png',
    qrImageBase64: 'base64data',
    lastReview: {
      screenshotPath: '/tmp/session/review.png',
    },
    lastRedeem: {
      screenshotPath: '/tmp/session/redeem.png',
      htmlPath: '/tmp/session/redeem.html',
      resultPath: '/tmp/session/redeem.json',
    },
  }

  assert.deepEqual(buildArtifactsPayload(session), {
    hasQrImage: true,
    hasReviewScreenshot: true,
    hasScreenshot: true,
  })

  assert.deepEqual(buildArtifactsPayload(session, { includeInternalPaths: true }), {
    hasQrImage: true,
    hasReviewScreenshot: true,
    hasScreenshot: true,
    sessionDir: '/tmp/session',
    qrImagePath: '/tmp/session/qq-qr.png',
    screenshotPath: '/tmp/session/redeem.png',
    htmlPath: '/tmp/session/redeem.html',
    resultPath: '/tmp/session/redeem.json',
    reviewScreenshotPath: '/tmp/session/review.png',
  })
})

test('normalizeTencentCookieMap fills required defaults and derived proxy cookie', () => {
  assert.deepEqual(
    normalizeTencentCookieMap([
      { name: 'openid', value: 'openid-1' },
      { name: 'access_token', value: 'token-1' },
      { name: '', value: 'ignored' },
    ]),
    {
      openid: 'openid-1',
      access_token: 'token-1',
      acctype: 'qc',
      appid: '101491592',
      iegams_milo_proxylogin_qc: '101491592_$$_openid-1_$$_token-1',
    },
  )
})

test('resolveInitialQrSessionState creates waiting-scan snapshot with ttl', () => {
  assert.deepEqual(
    resolveInitialQrSessionState('wx', {
      updatedAt: '2026-04-14T09:30:00.000Z',
      nowMs: 1_700_000_000_000,
      sessionTtlMs: 1_800_000,
    }),
    {
      status: 'waiting_scan',
      notice: '请使用微信扫码登录',
      updatedAt: '2026-04-14T09:30:00.000Z',
      expiresAt: 1_700_001_800_000,
    },
  )
})

test('ensureReviewScreenshot writes screenshot state and skips unchanged signature', async () => {
  const screenshotCalls = []
  const session = {
    sessionId: 'txbs-demo',
    loginType: 'qq',
    sessionDir: '/tmp/session-demo',
    page: {
      async screenshot(options) {
        screenshotCalls.push(options)
      },
    },
    lastReview: null,
  }

  const activityInfo = {
    role: {
      roleId: '1001',
      roleName: '测试角色',
    },
  }

  assert.equal(await ensureReviewScreenshot(session, activityInfo), true)
  assert.equal(screenshotCalls.length, 1)
  assert.equal(session.lastReview.roleId, '1001')
  assert.equal(session.lastReview.roleName, '测试角色')
  assert.equal(session.lastReview.signature, 'qq|1001|测试角色')
  assert.match(session.lastReview.screenshotPath, /role-review\.png$/)

  assert.equal(await ensureReviewScreenshot(session, activityInfo), false)
  assert.equal(screenshotCalls.length, 1)
})

test('ensureLoginTab clicks login UI and delegates to qq readiness', async () => {
  const calls = []
  const makeLocator = (name) => ({
    async count() {
      return 1
    },
    first() {
      const firstLocator = {
        async count() {
          return 1
        },
        async click(options) {
          calls.push({ type: 'click', name, options })
        },
      }
      return firstLocator
    },
  })

  const page = {
    locator(selector) {
      if (selector === '#unlogin') {
        return makeLocator('loginButton')
      }
      if (selector === '.qc-tab') {
        return makeLocator('qqTab')
      }
      throw new Error(`unexpected selector: ${selector}`)
    },
    async waitForTimeout(ms) {
      calls.push({ type: 'wait', ms })
    },
  }

  await ensureLoginTab(page, 'qq', {
    async ensureQqLoginReady() {
      calls.push({ type: 'ensure', name: 'qq' })
    },
  })

  assert.deepEqual(calls, [
    { type: 'click', name: 'loginButton', options: { timeout: 2_000 } },
    { type: 'click', name: 'qqTab', options: { timeout: 2_000 } },
    { type: 'wait', ms: 250 },
    { type: 'ensure', name: 'qq' },
  ])
})

test('captureQrImageWithRetry retries once after retryable error', async () => {
  const calls = []
  const page = {
    async waitForTimeout(ms) {
      calls.push({ type: 'wait', ms })
    },
  }

  let attempt = 0
  await captureQrImageWithRetry(page, 'wx', '/tmp/wx-qr.png', {
    async captureWxQrImage() {
      attempt += 1
      calls.push({ type: 'capture', attempt })
      if (attempt === 1) {
        throw new Error('Timeout while waiting for qr')
      }
    },
    async ensureLoginTab(_page, loginType) {
      calls.push({ type: 'ensureLoginTab', loginType })
    },
    isRetryableQrCaptureError(error) {
      return String(error.message).includes('Timeout')
    },
    logBrowserSessionDebug() {},
  })

  assert.equal(attempt, 2)
  assert.deepEqual(calls, [
    { type: 'capture', attempt: 1 },
    { type: 'wait', ms: 500 },
    { type: 'ensureLoginTab', loginType: 'wx' },
    { type: 'capture', attempt: 2 },
  ])
})

test('ensureLoggedInPresentation reloads page when logged-in host state is stale', async () => {
  const session = {
    page: { marker: 'page' },
    presentationSyncAttempts: 0,
  }
  const calls = []
  let hostCall = 0

  const result = await ensureLoggedInPresentation(session, {
    credentialReady: true,
    activityUrl: 'https://example.com/activity',
    async extractHostState() {
      hostCall += 1
      return hostCall === 1
        ? { unloginVisible: true, loginedVisible: false, loginText: '' }
        : { unloginVisible: false, loginedVisible: true, loginText: '已登录' }
    },
    async reloadActivityPageForPresentation(_page, activityUrl) {
      calls.push(activityUrl)
    },
  })

  assert.equal(session.presentationSyncAttempts, 1)
  assert.deepEqual(calls, ['https://example.com/activity'])
  assert.deepEqual(result, {
    reloaded: true,
    hostState: { unloginVisible: false, loginedVisible: true, loginText: '已登录' },
  })
})

test('ensureActivityInfoReady triggers render and polls until role becomes ready', async () => {
  const calls = []
  const page = {
    async waitForTimeout(ms) {
      calls.push({ type: 'wait', ms })
    },
  }
  const snapshots = [
    { role: { ready: false, roleName: '' } },
    { role: { ready: true, roleName: '测试角色' } },
  ]
  let nowValue = 0

  const result = await ensureActivityInfoReady(page, {
    triggerRender: true,
    timeoutMs: 1000,
    async triggerActivityRoleRender() {
      calls.push({ type: 'trigger' })
    },
    async extractActivityInfo() {
      calls.push({ type: 'extract' })
      return snapshots.shift()
    },
    now() {
      nowValue += 300
      return nowValue
    },
  })

  assert.deepEqual(calls, [
    { type: 'trigger' },
    { type: 'extract' },
    { type: 'wait', ms: 400 },
    { type: 'extract' },
  ])
  assert.deepEqual(result, { role: { ready: true, roleName: '测试角色' } })
})

test('extractActivityInfo falls back to empty structure when page evaluation fails', async () => {
  const page = {
    async evaluate() {
      throw new Error('page closed')
    },
  }

  assert.deepEqual(await extractActivityInfo(page), createEmptyActivityInfo())
})
