import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import type { ChildProcess } from 'child_process';

vi.mock('child_process', () => ({
  spawn: vi.fn(),
}));

class MockChildProcess extends EventEmitter {
  exitCode: number | null = null;
  killed = false;
  unref = vi.fn();
  kill = vi.fn((signal?: NodeJS.Signals) => {
    this.killed = true;
    this.emit('exit', signal === 'SIGKILL' ? 137 : 0, signal ?? null);
    return true;
  });
}

const mockSpawn = vi.mocked(spawn);

describe('gateway discovery', () => {
  let startGatewayDiscovery: typeof import('./discovery.js').startGatewayDiscovery;

  beforeAll(async () => {
    const mod = await import('./discovery.js');
    startGatewayDiscovery = mod.startGatewayDiscovery;
  });

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('starts avahi publisher with txt records', async () => {
    const child = new MockChildProcess();
    mockSpawn.mockReturnValueOnce(child as unknown as ChildProcess);
    setTimeout(() => child.emit('spawn'), 0);

    const handle = await startGatewayDiscovery({
      serviceName: 'flynn-gateway',
      serviceType: '_flynn._tcp',
      port: 18800,
      txtRecord: { instance: 'pid-123', version: '0.1.0' },
    });

    expect(mockSpawn).toHaveBeenCalledWith('avahi-publish-service', [
      'flynn-gateway',
      '_flynn._tcp',
      '18800',
      'instance=pid-123',
      'version=0.1.0',
    ], { stdio: 'ignore' });
    expect(child.unref).toHaveBeenCalledOnce();

    await handle.stop();
    expect(child.kill).toHaveBeenCalledWith('SIGTERM');
  });

  it('falls back to dns-sd when avahi is unavailable', async () => {
    const avahiChild = new MockChildProcess();
    const dnsChild = new MockChildProcess();
    mockSpawn.mockReturnValueOnce(avahiChild as unknown as ChildProcess);
    mockSpawn.mockReturnValueOnce(dnsChild as unknown as ChildProcess);

    setTimeout(() => avahiChild.emit('error', new Error('ENOENT')), 0);
    setTimeout(() => dnsChild.emit('spawn'), 0);

    const handle = await startGatewayDiscovery({
      serviceName: 'flynn-gateway',
      serviceType: '_flynn._tcp',
      port: 18800,
    });

    expect(mockSpawn).toHaveBeenNthCalledWith(2, 'dns-sd', [
      '-R',
      'flynn-gateway',
      '_flynn._tcp',
      'local',
      '18800',
    ], { stdio: 'ignore' });

    await handle.stop();
    expect(dnsChild.kill).toHaveBeenCalledWith('SIGTERM');
  });

  it('throws when no supported advertiser command is available', async () => {
    const avahiChild = new MockChildProcess();
    const dnsChild = new MockChildProcess();
    mockSpawn.mockReturnValueOnce(avahiChild as unknown as ChildProcess);
    mockSpawn.mockReturnValueOnce(dnsChild as unknown as ChildProcess);

    setTimeout(() => avahiChild.emit('error', new Error('ENOENT')), 0);
    setTimeout(() => dnsChild.emit('error', new Error('ENOENT')), 0);

    await expect(startGatewayDiscovery({
      serviceName: 'flynn-gateway',
      serviceType: '_flynn._tcp',
      port: 18800,
    })).rejects.toThrow(/Failed to start mDNS advertiser/);
  });
});
