/**
 * Shared E2E test helpers.
 *
 * - Login via API and hydrate localStorage so the Vue auth store picks up
 *   the tokens on page load (the store reads from localStorage on init).
 * - Server health check.
 * - Wait for a real LLM response in the chat view.
 */

import type { Page, expect as ExpectType } from '@playwright/test'

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/**
 * Backend port — read from BACKEND_PORT env var (set by the dev / E2E
 * environment) so E2E tests follow the same port allocation as the Vite
 * proxy and the running backend server. Default 18001 matches
 * agentkit.yaml / .env.dev. The Vite dev-server proxy is not available in
 * Node.js, so we target the backend directly.
 */
const BACKEND_PORT = process.env.BACKEND_PORT ?? '18001'

/**
 * Backend API base — absolute URL so fetch() works in both Node.js (Playwright
 * test context) and browser context.
 */
export const API_BASE = `http://127.0.0.1:${BACKEND_PORT}/api/v1`

/** Backend health endpoint (absolute URL for direct fetch). */
export const BACKEND_HEALTH_URL = `http://127.0.0.1:${BACKEND_PORT}/api/v1/health`

/** Test admin credentials — must match setup-test-user.py defaults. */
export const TEST_USER = {
  username: process.env.E2E_TEST_USERNAME ?? 'e2e_test_admin',
  password: process.env.E2E_TEST_PASSWORD ?? 'E2eTestPass123!',
  email: process.env.E2E_TEST_EMAIL ?? 'e2e-test@example.com',
} as const

/** localStorage keys used by the auth store (see stores/auth.ts). */
const ACCESS_TOKEN_KEY = 'agentkit.access_token'
const REFRESH_TOKEN_KEY = 'agentkit.refresh_token'
const USER_KEY = 'agentkit.user'

/** Max wait for a real LLM response (seconds → ms). */
export const LLM_RESPONSE_TIMEOUT_MS = 60_000

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface IAuthUser {
  id: string
  username: string
  email: string
  role: string
  is_active: boolean
  is_terminal_authorized: boolean
  is_server_terminal_authorized: boolean
}

interface ITokenPair {
  access_token: string
  refresh_token: string
  token_type: string
  expires_in: number
  user: IAuthUser
}

// ---------------------------------------------------------------------------
// Server health
// ---------------------------------------------------------------------------

/**
 * Poll the backend health endpoint until it responds 200 or times out.
 * Useful as a sanity check inside tests.
 */
export async function waitForServer(
  url: string = BACKEND_HEALTH_URL,
  timeoutMs = 30_000,
): Promise<void> {
  const deadline = Date.now() + timeoutMs
  while (Date.now() < deadline) {
    try {
      const resp = await fetch(url)
      if (resp.ok) return
    } catch {
      // not ready
    }
    await new Promise((r) => setTimeout(r, 1_000))
  }
  throw new Error(`Server at ${url} did not become healthy within ${timeoutMs}ms`)
}

// ---------------------------------------------------------------------------
// Login helpers
// ---------------------------------------------------------------------------

/**
 * Authenticate via the REST API and return the token pair.
 * Retries on 429 (rate limit) with exponential backoff.
 * Caches the token pair module-level so subsequent calls reuse it
 * (avoids triggering the server's rate limiter).
 * Throws on other non-200 responses.
 */
let _cachedTokenPair: ITokenPair | null = null

export async function loginViaApi(): Promise<ITokenPair> {
  // Return cached token if available (avoids rate limiting across tests).
  if (_cachedTokenPair) {
    return _cachedTokenPair
  }
  return _loginViaApiUncached()
}

/**
 * Force a fresh login, bypassing the module-level token cache.
 *
 * Necessary when a previous test (e.g. auth-persistence.spec.ts) corrupted
 * the persisted refresh token in localStorage AND the cached token pair is
 * no longer valid for the current browser context. Callers that hit a
 * cold-start 401 on /auth/whoami should retry with this.
 */
export async function forceRelogin(): Promise<ITokenPair> {
  _cachedTokenPair = null
  return _loginViaApiUncached()
}

async function _loginViaApiUncached(): Promise<ITokenPair> {
  const maxRetries = 5
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const resp = await fetch(`${API_BASE}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username: TEST_USER.username,
        password: TEST_USER.password,
      }),
    })

    if (resp.ok) {
      _cachedTokenPair = (await resp.json()) as ITokenPair
      return _cachedTokenPair
    }

    if (resp.status === 429 && attempt < maxRetries - 1) {
      // Rate limited — wait and retry (5s, 10s, 20s, 40s)
      const delayMs = 5000 * Math.pow(2, attempt)
      await new Promise((r) => setTimeout(r, delayMs))
      continue
    }

    const detail = await resp.text().catch(() => '<no body>')
    throw new Error(`Login failed (${resp.status}): ${detail}`)
  }
  throw new Error('Login failed: max retries exceeded')
}

/**
 * Log in via the API and hydrate localStorage so the Pinia auth store
 * picks up the tokens on the next page navigation.
 *
 * The auth store (stores/auth.ts) reads `agentkit.access_token`,
 * `agentkit.refresh_token`, and `agentkit.user` from localStorage on
 * construction, so populating these before navigating is sufficient.
 */
export async function loginAndHydrate(page: Page): Promise<ITokenPair> {
  const tokens = await loginViaApi()

  await page.goto('/login')

  await page.evaluate(
    ({ access, refresh, user }) => {
      localStorage.setItem('agentkit.access_token', access)
      localStorage.setItem('agentkit.refresh_token', refresh)
      localStorage.setItem('agentkit.user', JSON.stringify(user))
    },
    {
      access: tokens.access_token,
      refresh: tokens.refresh_token,
      user: tokens.user,
    },
  )

  return tokens
}

/**
 * Clear auth state from localStorage — useful for testing the
 * unauthenticated-redirect behaviour.
 */
export async function clearAuth(page: Page): Promise<void> {
  await page.evaluate(
    ({ access, refresh, user }) => {
      localStorage.removeItem(access)
      localStorage.removeItem(refresh)
      localStorage.removeItem(user)
    },
    {
      access: ACCESS_TOKEN_KEY,
      refresh: REFRESH_TOKEN_KEY,
      user: USER_KEY,
    },
  )
}

/**
 * Reload the page and wait for the auth cold-start sequence to settle.
 *
 * The auth store runs `startupCheck()` (whoami with the persisted refresh
 * token) on boot via `beginStartup()` in main.ts. The router guard awaits
 * `waitForStartup()` before deciding to redirect. So after `page.reload()`
 * we must wait until the URL has settled to either a protected route
 * (cold-start succeeded) or `/login` (cold-start failed).
 *
 * Returns the final URL for assertions.
 */
export async function reloadAndWaitAuth(
  page: Page,
  timeoutMs = 20_000,
): Promise<string> {
  await page.reload()
  // Wait for URL to settle — either /agent/* (auth ok) or /login (auth lost).
  // The cold-start probe can take a few seconds (whoami round-trip).
  const deadline = Date.now() + timeoutMs
  while (Date.now() < deadline) {
    const url = page.url()
    if (/\/(agent|login)/.test(url)) {
      // Give the router one extra tick to finalize the redirect.
      await page.waitForLoadState('networkidle', { timeout: 2_000 }).catch(() => {})
      return page.url()
    }
    await page.waitForTimeout(200)
  }
  throw new Error(
    `Auth did not settle after reload within ${timeoutMs}ms (last url: ${page.url()})`,
  )
}

/**
 * Overwrite the persisted refresh token with a fake value, simulating
 * a revoked / tampered token. Used to test the cold-start failure path.
 */
export async function corruptRefreshToken(page: Page): Promise<void> {
  await page.evaluate(
    ({ key }) => {
      localStorage.setItem(key, 'invalid-refresh-token-for-testing-only')
    },
    { key: REFRESH_TOKEN_KEY },
  )
}

// ---------------------------------------------------------------------------
// Chat helpers
// ---------------------------------------------------------------------------

/**
 * Wait for a real LLM response in the chat view.
 *
 * After sending a message, the assistant's response is rendered inside
 * `.message-shell--assistant .assistant-text__markdown`. While the LLM is
 * still streaming, the element may be empty or show a spinner. This helper
 * waits until the assistant message contains non-whitespace text.
 *
 * @param page      Playwright page
 * @param expect    The `expect` function from @playwright/test
 * @param timeoutMs Max wait time (default 60s for real LLM calls)
 */
export async function waitForLlmResponse(
  page: Page,
  expect: typeof ExpectType,
  timeoutMs = LLM_RESPONSE_TIMEOUT_MS,
): Promise<void> {
  // The assistant message content is rendered as sanitized HTML inside
  // .assistant-text__markdown. Wait for it to have non-empty text content.
  const assistantContent = page.locator(
    '.message-shell--assistant .assistant-text__markdown',
  )

  await expect
    .poll(
      async () => {
        // Check count first to avoid auto-wait on a non-existent element.
        const count = await assistantContent.count()
        if (count === 0) return 0
        const text = await assistantContent.last().textContent()
        return (text ?? '').trim().length
      },
      { timeout: timeoutMs, intervals: [1_000, 2_000, 5_000] },
    )
    .toBeGreaterThan(0)
}

/**
 * Send a chat message by typing into the textarea and pressing Enter.
 * Falls back to clicking the send button if Enter doesn't trigger send.
 */
export async function sendChatMessage(page: Page, message: string): Promise<void> {
  const textarea = page.getByPlaceholder('输入消息，按 Enter 发送...')
  await textarea.fill(message)
  await textarea.press('Enter')
}
