/**
 * CoordinationServer Tests
 *
 * Tests for the HTTP coordination server lifecycle, endpoints, and PID file management.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { CoordinationServer } from './index.js';
import { writeFile, unlink, mkdir, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import type { ProcessManager } from '../process/index.js';
import type { LogManager } from '../logging/index.js';
import type { EventBus, ServerStartedEvent, ServerStoppedEvent } from '../events/index.js';

/**
 * Creates a mock ProcessManager for testing.
 */
function createMockProcessManager(): ProcessManager {
  return {
    spawn: vi.fn(),
    stop: vi.fn(),
    stopAll: vi.fn().mockResolvedValue(undefined),
    restart: vi.fn(),
    isRunning: vi.fn(),
  } as unknown as ProcessManager;
}

/**
 * Creates a mock LogManager for testing.
 */
function createMockLogManager(): LogManager {
  return {
    ensureLogDir: vi.fn(),
    ensureProcessDir: vi.fn(),
    getProcessDir: vi.fn(),
    getLogPath: vi.fn(),
    listLogs: vi.fn(),
    cleanOldLogs: vi.fn(),
    getBaseDir: vi.fn(),
  } as unknown as LogManager;
}

/**
 * Creates a mock EventBus for testing.
 */
function createMockEventBus(): EventBus & { emit: ReturnType<typeof vi.fn> } {
  return {
    emit: vi.fn(),
    on: vi.fn(),
    off: vi.fn(),
    once: vi.fn(),
  } as unknown as EventBus & { emit: ReturnType<typeof vi.fn> };
}

/**
 * Gets a random high port to avoid conflicts.
 */
function getRandomPort(): number {
  return Math.floor(Math.random() * (65535 - 49152) + 49152);
}

/**
 * Gets a temporary PID file path.
 */
function getTempPidFile(): string {
  return join(tmpdir(), `cw-test-${randomUUID()}.pid`);
}

describe('CoordinationServer', () => {
  let server: CoordinationServer;
  let processManager: ProcessManager;
  let logManager: LogManager;
  let port: number;
  let pidFile: string;

  beforeEach(() => {
    processManager = createMockProcessManager();
    logManager = createMockLogManager();
    port = getRandomPort();
    pidFile = getTempPidFile();
    server = new CoordinationServer(
      { port, pidFile },
      processManager,
      logManager
    );
  });

  afterEach(async () => {
    // Clean up server if running
    if (server.isRunning()) {
      await server.stop();
    }
    // Clean up PID file if exists
    try {
      await unlink(pidFile);
    } catch {
      // Ignore if file doesn't exist
    }
  });

  describe('lifecycle', () => {
    it('should start listening on configured port', async () => {
      await server.start();

      expect(server.isRunning()).toBe(true);
      expect(server.getPort()).toBe(port);
    });

    it('should throw if already running', async () => {
      await server.start();

      await expect(server.start()).rejects.toThrow('Server is already running');
    });

    it('should throw if PID file exists from another server', async () => {
      // Create a PID file with current process PID (simulating another server)
      await mkdir(join(tmpdir()), { recursive: true });
      await writeFile(pidFile, process.pid.toString(), 'utf-8');

      await expect(server.start()).rejects.toThrow(
        /Another server appears to be running/
      );
    });

    it('should stop the server', async () => {
      await server.start();
      expect(server.isRunning()).toBe(true);

      await server.stop();
      expect(server.isRunning()).toBe(false);
    });

    it('should be idempotent when stopping (no error if not running)', async () => {
      expect(server.isRunning()).toBe(false);

      // Should not throw
      await server.stop();
      expect(server.isRunning()).toBe(false);
    });

    it('should report correct running state via isRunning()', async () => {
      expect(server.isRunning()).toBe(false);

      await server.start();
      expect(server.isRunning()).toBe(true);

      await server.stop();
      expect(server.isRunning()).toBe(false);
    });
  });

  describe('HTTP endpoints', () => {
    beforeEach(async () => {
      await server.start();
    });

    it('should return health status with uptime on GET /health', async () => {
      const response = await fetch(`http://127.0.0.1:${port}/health`);
      const data = await response.json();

      expect(response.status).toBe(200);
      expect(data.status).toBe('ok');
      expect(typeof data.uptime).toBe('number');
      expect(data.uptime).toBeGreaterThanOrEqual(0);
      expect(typeof data.processCount).toBe('number');
    });

    it('should return server info on GET /status', async () => {
      const response = await fetch(`http://127.0.0.1:${port}/status`);
      const data = await response.json();

      expect(response.status).toBe(200);
      expect(data.server).toBeDefined();
      expect(data.server.pid).toBe(process.pid);
      expect(typeof data.server.uptime).toBe('number');
      expect(typeof data.server.startedAt).toBe('string');
      expect(Array.isArray(data.processes)).toBe(true);
    });

    it('should return 404 on GET /unknown', async () => {
      const response = await fetch(`http://127.0.0.1:${port}/unknown`);
      const data = await response.json();

      expect(response.status).toBe(404);
      expect(data.error).toBe('Not found');
    });

    it('should return 405 on POST /health', async () => {
      const response = await fetch(`http://127.0.0.1:${port}/health`, {
        method: 'POST',
      });
      const data = await response.json();

      expect(response.status).toBe(405);
      expect(data.error).toBe('Method not allowed');
    });
  });

  describe('PID file management', () => {
    it('should create PID file on start', async () => {
      await server.start();

      const content = await readFile(pidFile, 'utf-8');
      expect(parseInt(content.trim(), 10)).toBe(process.pid);
    });

    it('should remove PID file on stop', async () => {
      await server.start();
      await server.stop();

      await expect(readFile(pidFile, 'utf-8')).rejects.toThrow(/ENOENT/);
    });

    it('should clean up stale PID file from dead process', async () => {
      // Create a stale PID file with a non-existent PID
      // Use a very high PID that's unlikely to exist
      const stalePid = 999999;
      await mkdir(join(tmpdir()), { recursive: true });
      await writeFile(pidFile, stalePid.toString(), 'utf-8');

      // Server should start successfully after cleaning up stale PID
      await server.start();

      expect(server.isRunning()).toBe(true);

      // Verify PID file now has our PID
      const content = await readFile(pidFile, 'utf-8');
      expect(parseInt(content.trim(), 10)).toBe(process.pid);
    });
  });

  describe('event emission', () => {
    let eventBus: ReturnType<typeof createMockEventBus>;
    let serverWithEvents: CoordinationServer;

    beforeEach(() => {
      eventBus = createMockEventBus();
      serverWithEvents = new CoordinationServer(
        { port, pidFile },
        processManager,
        logManager,
        eventBus
      );
    });

    afterEach(async () => {
      if (serverWithEvents.isRunning()) {
        await serverWithEvents.stop();
      }
    });

    it('should emit ServerStarted event on start', async () => {
      await serverWithEvents.start();

      expect(eventBus.emit).toHaveBeenCalledOnce();
      const emittedEvent = eventBus.emit.mock.calls[0][0] as ServerStartedEvent;
      expect(emittedEvent.type).toBe('server:started');
      expect(emittedEvent.payload.port).toBe(port);
      expect(emittedEvent.payload.host).toBe('127.0.0.1');
      expect(emittedEvent.payload.pid).toBe(process.pid);
      expect(emittedEvent.timestamp).toBeInstanceOf(Date);
    });

    it('should emit ServerStopped event on stop', async () => {
      await serverWithEvents.start();
      eventBus.emit.mockClear(); // Clear the start event

      await serverWithEvents.stop();

      expect(eventBus.emit).toHaveBeenCalledOnce();
      const emittedEvent = eventBus.emit.mock.calls[0][0] as ServerStoppedEvent;
      expect(emittedEvent.type).toBe('server:stopped');
      expect(typeof emittedEvent.payload.uptime).toBe('number');
      expect(emittedEvent.payload.uptime).toBeGreaterThanOrEqual(0);
      expect(emittedEvent.timestamp).toBeInstanceOf(Date);
    });

    it('should not emit events if eventBus is not provided', async () => {
      // Use the original server without eventBus
      await server.start();
      await server.stop();

      // eventBus.emit should never have been called since server doesn't have it
      expect(eventBus.emit).not.toHaveBeenCalled();
    });
  });
});
