import type { ChildProcessWithoutNullStreams } from 'node:child_process';
import { spawn } from 'node:child_process';

export interface WranglerDevOptions {
  cwd?: string;
  timeout?: number;
  env?: typeof process.env;
}

/**
 * Get the test worker URL and runner for E2E tests
 *
 * Two modes:
 * 1. CI: Uses TEST_WORKER_URL pointing to deployed worker
 * 2. Local: Spawns a new wrangler dev instance for this test file
 *
 * NOTE: wrangler.jsonc is auto-generated from wrangler.template.jsonc
 * by the test:e2e npm script before vitest runs.
 */
export async function getTestWorkerUrl(): Promise<{
  url: string;
  runner: WranglerDevRunner | null;
}> {
  // CI mode: use deployed worker URL
  if (process.env.TEST_WORKER_URL) {
    console.log('Using deployed test worker:', process.env.TEST_WORKER_URL);
    return { url: process.env.TEST_WORKER_URL, runner: null };
  }

  // Local mode: spawn wrangler dev for this test file
  // Config is auto-generated by test:e2e script before vitest runs
  const testWorkerDir = 'tests/e2e/test-worker';
  console.log('Spawning local wrangler dev...');
  const runner = new WranglerDevRunner({ cwd: testWorkerDir });
  const url = await runner.getUrl();
  return { url, runner };
}

/**
 * WranglerDevRunner - Manages wrangler dev lifecycle for integration tests
 *
 * Based on patterns from @cloudflare/containers test suite.
 *
 * Usage:
 * ```typescript
 * const runner = new WranglerDevRunner({ cwd: 'packages/sandbox' });
 * const url = await runner.getUrl();
 *
 * // Run tests against url...
 *
 * await runner.stop([sandboxId1, sandboxId2]);
 * ```
 */
export class WranglerDevRunner {
  private process: ChildProcessWithoutNullStreams;
  private stdout: string = '';
  private stderr: string = '';
  private url: string | null = null;
  private readonly timeout: number;
  private urlPromise: Promise<string>;

  constructor(private options: WranglerDevOptions = {}) {
    // Default to 5 minutes timeout to allow for Docker startup and initialization
    // After first run with pre-built image, startup is typically < 30s
    this.timeout = options.timeout || 300000;

    // Spawn wrangler dev
    this.process = spawn('npx', ['wrangler', 'dev'], {
      cwd: this.options.cwd,
      env: this.options.env || process.env,
      stdio: 'pipe'
    });

    // Capture stdout/stderr and extract URL
    this.urlPromise = new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(
          new Error(
            `Timeout after ${this.timeout}ms waiting for wrangler dev to be ready`
          )
        );
      }, this.timeout);

      this.process.stdout.on('data', (data: Buffer) => {
        const output = data.toString();
        this.stdout += output;
        console.log(output);

        // Check for ready pattern: "Ready on http://..."
        const match = output.match(/Ready on (?<url>https?:\/\/[^\s]+)/);
        if (match?.groups?.url && !this.url) {
          this.url = match.groups.url;
          clearTimeout(timeoutId);
          resolve(this.url);
        }
      });

      this.process.stderr.on('data', (data: Buffer) => {
        this.stderr += data.toString();
        console.log(data.toString());
      });

      this.process.on('error', (error) => {
        clearTimeout(timeoutId);
        reject(error);
      });

      this.process.on('exit', (code, signal) => {
        if (code !== 0 && code !== null) {
          clearTimeout(timeoutId);
          reject(
            new Error(
              `Wrangler dev exited with code ${code} before becoming ready`
            )
          );
        }
        if (signal) {
          clearTimeout(timeoutId);
          reject(
            new Error(
              `Wrangler dev killed by signal ${signal} before becoming ready`
            )
          );
        }
      });
    });
  }

  /**
   * Get captured stdout (useful for verifying lifecycle hooks)
   */
  getStdout(): string {
    return this.stdout;
  }

  /**
   * Get captured stderr
   */
  getStderr(): string {
    return this.stderr;
  }

  /**
   * Wait for wrangler dev to be ready and return the URL
   */
  async getUrl(): Promise<string> {
    return this.urlPromise;
  }

  /**
   * Stop wrangler dev process
   *
   * Note: Container cleanup is now handled by afterEach hooks calling cleanupSandbox()
   * directly, so this method only needs to stop the wrangler process.
   *
   * Cleanup sequence:
   * 1. Send SIGTERM to wrangler process
   * 2. Wait up to 5 seconds for graceful shutdown
   * 3. Send SIGKILL if still running
   */
  async stop(): Promise<void> {
    if (!this.process || this.process.killed) {
      console.log('Wrangler process already stopped');
      return;
    }

    console.log('Stopping wrangler dev process...');

    // Send SIGTERM for graceful shutdown
    this.process.kill('SIGTERM');

    // Wait for process to exit (with timeout)
    const exitPromise = new Promise<void>((resolve) => {
      this.process.on('close', () => {
        console.log('Wrangler process stopped gracefully');
        resolve();
      });
    });

    const timeoutPromise = new Promise<void>((resolve) => {
      setTimeout(() => {
        if (!this.process.killed) {
          console.warn(
            'Wrangler process did not stop gracefully, sending SIGKILL'
          );
          this.process.kill('SIGKILL');
        }
        resolve();
      }, 5000);
    });

    await Promise.race([exitPromise, timeoutPromise]);
  }
}
