import { spawn, type ChildProcess } from 'node:child_process';
import { once } from 'node:events';
import { createServer } from 'node:net';
import { join } from 'node:path';

export interface TestServerHandle {
  port: number;
  url: string;
  stop: () => Promise<void>;
}

const E2E_DIR = join(__dirname, '..');
const START_SERVER_SCRIPT = join(E2E_DIR, 'helpers', 'start-test-server.js');

export async function startTestServer(retries = 3): Promise<TestServerHandle> {
  for (let attempt = 1; attempt <= retries; attempt++) {
    const port = await allocatePort();
    const child = spawn(process.execPath, [START_SERVER_SCRIPT], {
      cwd: E2E_DIR,
      env: {
        ...process.env,
        TEST_SERVER_PORT: String(port)
      },
      stdio: 'pipe'
    });

    child.stdout?.on('data', (chunk: Buffer | string) => {
      process.stdout.write(chunk.toString());
    });

    child.stderr?.on('data', (chunk: Buffer | string) => {
      process.stderr.write(chunk.toString());
    });

    try {
      await waitForServerReady(port, child);
    } catch (error) {
      await stopServer(child);

      if (attempt < retries) {
        console.log(`[E2E Server] Attempt ${attempt} failed, retrying...`);
        continue;
      }

      throw error;
    }

    return {
      port,
      url: `http://localhost:${port}`,
      stop: async () => {
        await stopServer(child);
      }
    };
  }

  throw new Error('startTestServer: unreachable');
}

async function allocatePort(): Promise<number> {
  return await new Promise<number>((resolve, reject) => {
    const probe = createServer();

    probe.once('error', reject);
    probe.listen(0, '127.0.0.1', () => {
      const address = probe.address();

      if (!address || typeof address === 'string') {
        probe.close();
        reject(new Error('Failed to resolve an ephemeral test server port'));
        return;
      }

      const { port } = address;

      probe.close((error) => {
        if (error) {
          reject(error);
          return;
        }

        resolve(port);
      });
    });
  });
}

async function waitForServerReady(port: number, child: ChildProcess, timeoutMs = 30_000): Promise<void> {
  const readyUrl = `http://127.0.0.1:${port}/api/servers?limit=1`;
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    if (child.exitCode !== null) {
      throw new Error(`Test server exited before becoming ready (exit code ${child.exitCode})`);
    }

    try {
      const response = await fetch(readyUrl);

      if (response.ok) {
        return;
      }
    } catch {
      // Server still starting.
    }

    await wait(250);
  }

  throw new Error(`Timed out waiting for test server on port ${port}`);
}

async function stopServer(child: ChildProcess): Promise<void> {
  if (child.exitCode !== null) {
    return;
  }

  child.kill('SIGTERM');

  const exited = await Promise.race([once(child, 'exit').then(() => true), wait(3_000).then(() => false)]);

  if (!exited && child.exitCode === null) {
    child.kill('SIGKILL');
    await once(child, 'exit');
  }
}

function wait(durationMs: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, durationMs);
  });
}
