// this is automatically detected by playground/vitestSetup.ts and will replace
// the default e2e test serve behavior

import { execaCommand } from 'execa'
import kill from 'kill-port'
import {
  isBuild,
  isWindows,
  killProcess,
  ports,
  rootDir,
  viteBinPath,
} from '~utils'

export const port = ports.cli

export async function serve() {
  // collect stdout and stderr streams from child processes here to avoid interfering with regular vitest output
  const streams = {
    build: { out: [], err: [] },
    server: { out: [], err: [] },
  }
  // helpers to collect streams
  const collectStreams = (name, process) => {
    process.stdout.on('data', (d) => streams[name].out.push(d.toString()))
    process.stderr.on('data', (d) => streams[name].err.push(d.toString()))
  }
  const collectErrorStreams = (name, e) => {
    e.stdout && streams[name].out.push(e.stdout)
    e.stderr && streams[name].err.push(e.stderr)
  }

  // helper to output stream content on error
  const printStreamsToConsole = async (name) => {
    const std = streams[name]
    if (std.out && std.out.length > 0) {
      console.log(`stdout of ${name}\n${std.out.join('\n')}\n`)
    }
    if (std.err && std.err.length > 0) {
      console.log(`stderr of ${name}\n${std.err.join('\n')}\n`)
    }
  }

  // only run `vite build` when needed
  if (isBuild) {
    const buildCommand = `${viteBinPath} build`
    try {
      const buildProcess = execaCommand(buildCommand, {
        cwd: rootDir,
        stdio: 'pipe',
      })
      collectStreams('build', buildProcess)
      await buildProcess
    } catch (e) {
      console.error(`error while executing cli command "${buildCommand}":`, e)
      collectErrorStreams('build', e)
      await printStreamsToConsole('build')
      throw e
    }
  }

  await kill(port)

  // run `vite --port x` or `vite preview --port x` to start server
  const viteServerArgs = ['--port', `${port}`, '--strict-port']
  if (isBuild) {
    viteServerArgs.unshift('preview')
  }
  const serverCommand = `${viteBinPath} ${viteServerArgs.join(' ')}`
  const serverProcess = execaCommand(serverCommand, {
    cwd: rootDir,
    stdio: 'pipe',
  })
  collectStreams('server', serverProcess)

  // close server helper, send SIGTERM followed by SIGKILL if needed, give up after 3sec
  const close = async () => {
    if (serverProcess) {
      const timeoutError = `server process still alive after 3s`
      try {
        await killProcess(serverProcess)
        await resolvedOrTimeout(serverProcess, 5173, timeoutError)
      } catch (e) {
        if (e === timeoutError || (!serverProcess.killed && !isWindows)) {
          collectErrorStreams('server', e)
          console.error(
            `error while killing cli command "${serverCommand}":`,
            e,
          )
          await printStreamsToConsole('server')
        }
      }
    }
  }

  try {
    await startedOnPort(serverProcess, port, 5173)
    return { close }
  } catch (e) {
    collectErrorStreams('server', e)
    console.error(`error while executing cli command "${serverCommand}":`, e)
    await printStreamsToConsole('server')
    try {
      await close()
    } catch (e1) {
      console.error(
        `error while killing cli command after failed execute "${serverCommand}":`,
        e1,
      )
    }
  }
}

// helper to validate that server was started on the correct port
async function startedOnPort(serverProcess, port, timeout) {
  let checkPort
  const startedPromise = new Promise<void>((resolve, reject) => {
    checkPort = (data) => {
      const str = data.toString()
      // hack, console output may contain color code gibberish
      // skip gibberish between localhost: and port number
      const match = str.match(
        /(http:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):).*(\d{4})/,
      )
      if (match) {
        const startedPort = parseInt(match[2], 10)
        if (startedPort === port) {
          resolve()
        } else {
          const msg = `server listens on port ${startedPort} instead of ${port}`
          reject(msg)
        }
      }
    }
    serverProcess.stdout.on('data', checkPort)
  })
  return resolvedOrTimeout(
    startedPromise,
    timeout,
    `failed to start within ${timeout}ms`,
  ).finally(() => serverProcess.stdout.off('data', checkPort))
}

// helper function that rejects with errorMessage if promise isn't settled within ms
async function resolvedOrTimeout(promise, ms, errorMessage) {
  let timer
  return Promise.race([
    promise,
    new Promise((_, reject) => {
      timer = setTimeout(() => reject(errorMessage), ms)
    }),
  ]).finally(() => {
    clearTimeout(timer)
    timer = null
  })
}
