import { describe, test, expect } from 'bun:test';
import * as net from 'net';
import * as path from 'path';
import { __testInternals__ } from '../src/server';

const polyfillPath = path.resolve(import.meta.dir, '../src/bun-polyfill.cjs');

// Helper: bind a port and hold it open, returning a cleanup function
function occupyPort(port: number): Promise<() => Promise<void>> {
  return new Promise((resolve, reject) => {
    const srv = net.createServer();
    srv.once('error', reject);
    srv.listen(port, '127.0.0.1', () => {
      resolve(() => new Promise<void>((r) => srv.close(() => r())));
    });
  });
}

// Helper: find a known-free port by binding to 0
function getFreePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const srv = net.createServer();
    srv.once('error', reject);
    srv.listen(0, '127.0.0.1', () => {
      const port = (srv.address() as net.AddressInfo).port;
      srv.close(() => resolve(port));
    });
  });
}

describe('findPort / isPortAvailable', () => {
  test('explicit BROWSE_PORT diagnostic distinguishes bind denial from occupied port', () => {
    const blocked = __testInternals__.formatExplicitPortUnavailableError(34567, {
      available: false,
      code: 'EPERM',
      message: 'operation not permitted',
    }).message;

    expect(blocked).toContain('Cannot bind BROWSE_PORT=34567');
    expect(blocked).toContain('localhost port binding is blocked');
    expect(blocked).toContain('not that the port is occupied');

    const occupied = __testInternals__.formatExplicitPortUnavailableError(34567, {
      available: false,
      code: 'EADDRINUSE',
      message: 'address already in use',
    }).message;

    expect(occupied).toBe('[browse] Port 34567 (from BROWSE_PORT env) is in use');
  });

  test('random port diagnostic calls out sandbox-style bind denial', () => {
    const message = __testInternals__.formatRandomPortUnavailableError([
      { port: 11001, result: { available: false, code: 'EADDRINUSE', message: 'address already in use' } },
      { port: 12002, result: { available: false, code: 'EPERM', message: 'operation not permitted' } },
    ]).message;

    expect(message).toContain('Cannot bind localhost ports after 2 attempts');
    expect(message).toContain('Last error: 12002 (EPERM: operation not permitted)');
    expect(message).toContain('not that every sampled port is occupied');
    expect(message).toContain('set BROWSE_PORT to an approved port');
  });

  test('random port diagnostic preserves old busy-port meaning when all attempts are occupied', () => {
    const message = __testInternals__.formatRandomPortUnavailableError([
      { port: 11001, result: { available: false, code: 'EADDRINUSE', message: 'address already in use' } },
      { port: 12002, result: { available: false, code: 'EADDRINUSE', message: 'address already in use' } },
    ]).message;

    expect(message).toContain('No available port after 5 attempts');
    expect(message).toContain('every sampled port was already in use');
  });

  test('isPortAvailable returns true for a free port', async () => {
    // Use the same isPortAvailable logic from server.ts
    const port = await getFreePort();

    const available = await new Promise<boolean>((resolve) => {
      const srv = net.createServer();
      srv.once('error', () => resolve(false));
      srv.listen(port, '127.0.0.1', () => {
        srv.close(() => resolve(true));
      });
    });

    expect(available).toBe(true);
  });

  test('isPortAvailable returns false for an occupied port', async () => {
    const port = await getFreePort();
    const release = await occupyPort(port);

    try {
      const available = await new Promise<boolean>((resolve) => {
        const srv = net.createServer();
        srv.once('error', () => resolve(false));
        srv.listen(port, '127.0.0.1', () => {
          srv.close(() => resolve(true));
        });
      });

      expect(available).toBe(false);
    } finally {
      await release();
    }
  });

  test('port is actually free after isPortAvailable returns true', async () => {
    // This is the core race condition test: after isPortAvailable says
    // a port is free, can we IMMEDIATELY bind to it?
    const port = await getFreePort();

    // Simulate isPortAvailable
    const isFree = await new Promise<boolean>((resolve) => {
      const srv = net.createServer();
      srv.once('error', () => resolve(false));
      srv.listen(port, '127.0.0.1', () => {
        srv.close(() => resolve(true));
      });
    });

    expect(isFree).toBe(true);

    // Now immediately try to bind — this would fail with the old
    // Bun.serve() polyfill approach because the test server's
    // listen() would still be pending
    const canBind = await new Promise<boolean>((resolve) => {
      const srv = net.createServer();
      srv.once('error', () => resolve(false));
      srv.listen(port, '127.0.0.1', () => {
        srv.close(() => resolve(true));
      });
    });

    expect(canBind).toBe(true);
  });

  test('polyfill Bun.serve stop() is fire-and-forget (async)', async () => {
    // Verify that the polyfill's stop() does NOT wait for the socket
    // to actually close — this is the root cause of the race condition.
    // On macOS/Linux the OS reclaims the port fast enough that the race
    // rarely manifests, but on Windows TIME_WAIT makes it 100% repro.
    const result = Bun.spawnSync(['node', '-e', `
      require('${polyfillPath}');
      const net = require('net');

      async function test() {
        const port = 10000 + Math.floor(Math.random() * 50000);

        const testServer = Bun.serve({
          port,
          hostname: '127.0.0.1',
          fetch: () => new Response('ok'),
        });

        // stop() returns undefined — it does NOT return a Promise,
        // so callers cannot await socket teardown
        const retval = testServer.stop();
        console.log(typeof retval === 'undefined' ? 'FIRE_AND_FORGET' : 'AWAITABLE');
      }

      test();
    `], { stdout: 'pipe', stderr: 'pipe' });

    const output = result.stdout.toString().trim();
    // Confirms the polyfill's stop() is fire-and-forget — callers
    // cannot wait for the port to be released, hence the race
    expect(output).toBe('FIRE_AND_FORGET');
  });

  test('net.createServer approach does not have the race condition', async () => {
    // Prove the fix: net.createServer with proper async bind/close
    // releases the port cleanly
    const result = Bun.spawnSync(['node', '-e', `
      const net = require('net');

      async function testFix() {
        const port = 10000 + Math.floor(Math.random() * 50000);

        // Simulate the NEW isPortAvailable: proper async bind/close
        const isFree = await new Promise((resolve) => {
          const srv = net.createServer();
          srv.once('error', () => resolve(false));
          srv.listen(port, '127.0.0.1', () => {
            srv.close(() => resolve(true));
          });
        });

        if (!isFree) {
          console.log('PORT_BUSY');
          return;
        }

        // Immediately try to bind — should succeed because close()
        // completed before the Promise resolved
        const canBind = await new Promise((resolve) => {
          const srv = net.createServer();
          srv.once('error', () => resolve(false));
          srv.listen(port, '127.0.0.1', () => {
            srv.close(() => resolve(true));
          });
        });

        console.log(canBind ? 'FIX_WORKS' : 'FIX_BROKEN');
      }

      testFix();
    `], { stdout: 'pipe', stderr: 'pipe' });

    const output = result.stdout.toString().trim();
    expect(output).toBe('FIX_WORKS');
  });

  test('isPortAvailable handles rapid sequential checks', async () => {
    // Stress test: check the same port multiple times in sequence
    const port = await getFreePort();
    const results: boolean[] = [];

    for (let i = 0; i < 5; i++) {
      const available = await new Promise<boolean>((resolve) => {
        const srv = net.createServer();
        srv.once('error', () => resolve(false));
        srv.listen(port, '127.0.0.1', () => {
          srv.close(() => resolve(true));
        });
      });
      results.push(available);
    }

    // All 5 checks should succeed — no leaked sockets
    expect(results).toEqual([true, true, true, true, true]);
  });
});
