import {
  Before,
  After,
  BeforeAll,
  AfterAll,
  setDefaultTimeout,
} from '@cucumber/cucumber'
import { spawn, type ChildProcess } from 'child_process'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import type { AppWorld } from './world.js'
import { config } from './types.js'

setDefaultTimeout(config.responseTimeout)

let backendProcess: ChildProcess | undefined
let frontendProcess: ChildProcess | undefined

async function waitForUrl(url: string, label: string, timeoutMs = 60_000) {
  const deadline = Date.now() + timeoutMs
  while (Date.now() < deadline) {
    try {
      const res = await fetch(url)
      if (res.status < 500) return
    } catch {
      // not ready yet
    }
    await new Promise((r) => setTimeout(r, 500))
  }
  throw new Error(`[e2e] ${label} did not become ready at ${url} within ${timeoutMs}ms`)
}

BeforeAll(async function () {
  if (!config.manageServers) {
    // Caller is expected to start backend + frontend (e.g. via pm2, docker,
    // or a separate `yarn dev` terminal) before running tests.
    return
  }

  const __dirname = dirname(fileURLToPath(import.meta.url))
  const repoRoot = resolve(__dirname, '../../..')

  // Project-specific: override these commands by setting E2E_BACKEND_CMD /
  // E2E_FRONTEND_CMD env vars. The defaults assume `yarn dev` at the repo
  // root spawns the frontend, and the project provides a `dev:backend`
  // script for the API.
  const backendCmd = process.env.E2E_BACKEND_CMD ?? 'yarn dev:backend'
  const frontendCmd = process.env.E2E_FRONTEND_CMD ?? 'yarn dev'

  backendProcess = spawn(backendCmd, {
    cwd: repoRoot,
    env: { ...process.env, NODE_ENV: 'test' },
    stdio: 'pipe',
    shell: true,
    detached: true,
  })
  backendProcess.stderr?.on('data', (d: Buffer) =>
    process.stderr.write(`[backend] ${d}`),
  )
  backendProcess.stdout?.on('data', (d: Buffer) =>
    process.stdout.write(`[backend] ${d}`),
  )

  frontendProcess = spawn(frontendCmd, {
    cwd: repoRoot,
    env: { ...process.env, NODE_ENV: 'test' },
    stdio: 'pipe',
    shell: true,
    detached: true,
  })
  frontendProcess.stderr?.on('data', (d: Buffer) =>
    process.stderr.write(`[frontend] ${d}`),
  )
  frontendProcess.stdout?.on('data', (d: Buffer) =>
    process.stdout.write(`[frontend] ${d}`),
  )

  await Promise.all([
    waitForUrl(config.apiUrl, 'backend'),
    waitForUrl(config.appUrl, 'frontend'),
  ])
})

AfterAll(async function () {
  for (const p of [backendProcess, frontendProcess]) {
    if (!p?.pid) continue
    try {
      // Detached + pgid kill takes the whole tree (vite/wrangler spawn children).
      process.kill(-p.pid, 'SIGTERM')
    } catch {
      try {
        p.kill('SIGTERM')
      } catch {
        // ignore
      }
    }
  }
})

Before(async function (this: AppWorld) {
  await this.openBrowser()
  if (config.resetUrl) {
    const body = config.resetRpcName
      ? JSON.stringify({ rpcName: config.resetRpcName, data: {} })
      : undefined
    await fetch(config.resetUrl, {
      method: 'POST',
      headers: body ? { 'content-type': 'application/json' } : undefined,
      body,
    })
      .then((res) => {
        if (!res.ok) {
          process.stderr.write(
            `[e2e] WARN: reset hook ${config.resetUrl} returned ${res.status}\n`,
          )
        }
      })
      .catch(() => {
        process.stderr.write(`[e2e] WARN: reset hook ${config.resetUrl} unreachable\n`)
      })
  }
})

After(async function (this: AppWorld) {
  await this.closeBrowser()
})
