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

setDefaultTimeout(Math.max(config.responseTimeout, 120_000))

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`)
}

function startProcess({
  cmd,
  cwd,
  env,
  label,
}: {
  cmd: string
  cwd: string
  env: NodeJS.ProcessEnv
  label: string
}) {
  const p = spawn(cmd, {
    cwd,
    env,
    stdio: 'pipe',
    shell: true,
    detached: true,
  })
  p.stderr?.on('data', (d: Buffer) => process.stderr.write(`[${label}] ${d}`))
  p.stdout?.on('data', (d: Buffer) => process.stdout.write(`[${label}] ${d}`))
  return p
}

async function stopProcess(p: ChildProcess | undefined) {
  if (!p?.pid) return
  try {
    process.kill(-p.pid, 'SIGTERM')
  } catch {
    try {
      p.kill('SIGTERM')
    } catch {
      // ignore
    }
  }
  await new Promise((r) => setTimeout(r, 1_000))
}

function killPort(port: number) {
  try {
    const out = execFileSync('lsof', ['-ti', `tcp:${port}`], {
      encoding: 'utf8',
    }).trim()
    if (!out) return
    for (const pid of out.split(/\s+/)) {
      if (!pid) continue
      try {
        process.kill(Number(pid), 'SIGTERM')
      } catch {
        // ignore
      }
    }
  } catch {
    // ignore
  }
}

const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(__dirname, '../../..')
const seedFile = join(repoRoot, 'db', 'sqlite-seed.sql')
const sqliteDb = join(repoRoot, '.pikku-runtime', 'dev.db')

function prepareDatabase() {
  execFileSync('sqlite3', [sqliteDb], {
    cwd: repoRoot,
    input: readFileSync(seedFile),
    stdio: ['pipe', 'inherit', 'inherit'],
  })
}

BeforeAll(async function () {
  if (!config.manageServers) return

  if (process.env.E2E_PREPARE_DB !== '0' && process.env.E2E_PREPARE_DB !== 'false') {
    prepareDatabase()
  }

  killPort(3000)
  killPort(3001)

  backendProcess = startProcess({
    cmd: process.env.E2E_BACKEND_CMD ?? 'bun run dev:backend',
    cwd: repoRoot,
    env: {
      ...process.env,
      NODE_ENV: 'test',
    },
    label: 'backend',
  })
  await waitForUrl(`${config.apiUrl}/api/auth/get-session`, 'backend')

  frontendProcess = startProcess({
    cmd: process.env.E2E_FRONTEND_CMD ?? 'bun run dev:frontend',
    cwd: repoRoot,
    env: {
      ...process.env,
      NODE_ENV: 'test',
      VITE_API_BASE_URL: config.apiUrl,
    },
    label: 'frontend',
  })
  await waitForUrl(`${config.appUrl}/en`, 'frontend')
})

AfterAll(async function () {
  await stopProcess(frontendProcess)
  await stopProcess(backendProcess)
})

Before(async function (this: AppWorld) {
  await this.openBrowser()
})

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