import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventEmitter } from 'node:events';
import type { ChildProcess } from 'node:child_process';
import { ManagedVllmProvider } from '../src/providers/vllm-managed.js';
import type { ManagedVllmConfig } from '../src/providers/vllm-managed.js';

/** Create a fake ChildProcess with controllable exit and streams. */
function createFakeProcess(): ChildProcess & { _emit: (event: string, ...args: unknown[]) => void } {
  const proc = new EventEmitter() as ChildProcess & { _emit: (event: string, ...args: unknown[]) => void };
  (proc as Record<string, unknown>).pid = 12345;
  (proc as Record<string, unknown>).killed = false;
  (proc as Record<string, unknown>).exitCode = null;
  proc.kill = vi.fn(() => {
    (proc as Record<string, unknown>).killed = true;
    return true;
  });
  proc.stderr = new EventEmitter() as ChildProcess['stderr'];
  proc.stdout = new EventEmitter() as ChildProcess['stdout'];
  proc._emit = (event: string, ...args: unknown[]) => proc.emit(event, ...args);
  return proc;
}

function createProvider(overrides?: Partial<ManagedVllmConfig>): {
  provider: ManagedVllmProvider;
  fakeProcess: ReturnType<typeof createFakeProcess>;
  healthCheckFn: ReturnType<typeof vi.fn>;
  spawnFn: ReturnType<typeof vi.fn>;
} {
  const fakeProcess = createFakeProcess();
  const healthCheckFn = vi.fn<[number], Promise<boolean>>().mockResolvedValue(false);
  const spawnFn = vi.fn().mockReturnValue(fakeProcess);

  const provider = new ManagedVllmProvider({
    venvPath: '/tmp/test-venv',
    model: 'test-model',
    port: 9999,
    idleTimeoutMinutes: 1,
    spawnFn: spawnFn as unknown as ManagedVllmConfig['spawnFn'],
    healthCheckFn,
    ...overrides,
  });

  return { provider, fakeProcess, healthCheckFn, spawnFn };
}

describe('ManagedVllmProvider', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe('initial state', () => {
    it('starts in stopped state', () => {
      const { provider } = createProvider();
      const status = provider.getStatus();
      expect(status.state).toBe('stopped');
      expect(status.pid).toBeNull();
      expect(status.uptime).toBeNull();
      expect(status.lastError).toBeNull();
    });

    it('reports name as vllm-managed', () => {
      const { provider } = createProvider();
      expect(provider.name).toBe('vllm-managed');
    });
  });

  describe('isAvailable', () => {
    it('returns true when stopped (can auto-start)', async () => {
      const { provider } = createProvider();
      expect(await provider.isAvailable()).toBe(true);
    });

    it('returns true when running', async () => {
      const { provider, healthCheckFn } = createProvider();
      healthCheckFn.mockResolvedValue(true);
      // Force state to running
      (provider as unknown as Record<string, string>).state = 'running';
      expect(await provider.isAvailable()).toBe(true);
    });

    it('returns false when in error state', async () => {
      const { provider } = createProvider();
      (provider as unknown as Record<string, string>).state = 'error';
      expect(await provider.isAvailable()).toBe(false);
    });
  });

  describe('ensureRunning', () => {
    it('spawns vllm with correct args', async () => {
      const { provider, spawnFn, healthCheckFn } = createProvider();

      // Health check succeeds on first poll
      healthCheckFn.mockResolvedValue(true);

      const promise = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      // Advance past poll interval
      await vi.advanceTimersByTimeAsync(2100);
      await promise;

      expect(spawnFn).toHaveBeenCalledOnce();
      const [bin, args, opts] = spawnFn.mock.calls[0] as [string, string[], Record<string, unknown>];
      expect(bin).toBe('/tmp/test-venv/bin/vllm');
      expect(args).toContain('serve');
      expect(args).toContain('test-model');
      expect(args).toContain('--port');
      expect(args).toContain('9999');
      expect(args).toContain('--gpu-memory-utilization');
      expect(args).toContain('0.75');
      expect(args).toContain('--max-model-len');
      expect(args).toContain('4096');

      const env = (opts as Record<string, Record<string, string>>).env;
      expect(env['VIRTUAL_ENV']).toBe('/tmp/test-venv');
      expect(env['LD_LIBRARY_PATH']).toContain('/usr/lib64/nvidia');
    });

    it('sets state to running after health check passes', async () => {
      const { provider, healthCheckFn } = createProvider();
      healthCheckFn.mockResolvedValue(true);

      const promise = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      await vi.advanceTimersByTimeAsync(2100);
      await promise;

      expect(provider.getStatus().state).toBe('running');
      expect(provider.getStatus().pid).toBe(12345);
    });

    it('sets state to error when process exits during startup', async () => {
      const { provider, fakeProcess, healthCheckFn } = createProvider();
      healthCheckFn.mockResolvedValue(false);

      const promise = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      // Attach a no-op catch to prevent unhandled rejection warning
      promise.catch(() => {});

      // Simulate process exit
      (fakeProcess as Record<string, unknown>).exitCode = 1;
      fakeProcess._emit('exit', 1);

      // Advance past poll interval
      await vi.advanceTimersByTimeAsync(2100);

      await expect(promise).rejects.toThrow();
      expect(provider.getStatus().state).toBe('error');
    });

    it('reuses running process on subsequent calls', async () => {
      const { provider, spawnFn, healthCheckFn } = createProvider();
      healthCheckFn.mockResolvedValue(true);

      const p1 = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      await vi.advanceTimersByTimeAsync(2100);
      await p1;

      // Second call — should not spawn again
      await (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      expect(spawnFn).toHaveBeenCalledOnce();
    });

    it('coalesces concurrent startup calls', async () => {
      const { provider, spawnFn, healthCheckFn } = createProvider();
      healthCheckFn.mockResolvedValue(true);

      const p1 = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      const p2 = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      await vi.advanceTimersByTimeAsync(2100);
      await Promise.all([p1, p2]);

      expect(spawnFn).toHaveBeenCalledOnce();
    });
  });

  describe('complete', () => {
    it('starts vLLM then delegates to inner provider', async () => {
      const { provider, healthCheckFn } = createProvider();
      healthCheckFn.mockResolvedValue(true);

      // We can't easily mock the inner OpenAiProvider's HTTP calls,
      // but we can verify ensureRunning was called by checking state
      const promise = provider.complete({
        messages: [{ role: 'user', content: 'hello' }],
      });
      await vi.advanceTimersByTimeAsync(2100);

      // The complete will fail because the inner OpenAiProvider tries HTTP,
      // but we can verify the provider started
      try {
        await promise;
      } catch {
        // Expected — inner provider can't reach localhost:9999
      }

      expect(provider.getStatus().state).toBe('running');
    });
  });

  describe('listModels', () => {
    it('returns configured model when stopped', async () => {
      const { provider } = createProvider();
      const models = await provider.listModels();
      expect(models).toEqual(['test-model']);
    });
  });

  describe('idle timeout', () => {
    it('stops process after idle timeout', async () => {
      const { provider, healthCheckFn, fakeProcess } = createProvider({ idleTimeoutMinutes: 1 });
      healthCheckFn.mockResolvedValue(true);

      // Start the provider
      const promise = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      await vi.advanceTimersByTimeAsync(2100);
      await promise;

      expect(provider.getStatus().state).toBe('running');

      // Advance past idle timeout (1 min) + check interval (30s)
      await vi.advanceTimersByTimeAsync(90_000);

      expect(provider.getStatus().state).toBe('stopped');
      expect(fakeProcess.kill).toHaveBeenCalled();
    });
  });

  describe('restart after stop', () => {
    it('can restart after being stopped by idle timeout', async () => {
      const { provider, spawnFn, healthCheckFn } = createProvider({ idleTimeoutMinutes: 1 });
      healthCheckFn.mockResolvedValue(true);

      // Start
      const p1 = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      await vi.advanceTimersByTimeAsync(2100);
      await p1;
      expect(provider.getStatus().state).toBe('running');

      // Idle stop
      await vi.advanceTimersByTimeAsync(90_000);
      expect(provider.getStatus().state).toBe('stopped');

      // Create a new fake process for restart
      const newProc = createFakeProcess();
      spawnFn.mockReturnValue(newProc);

      // Restart
      const p2 = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      await vi.advanceTimersByTimeAsync(2100);
      await p2;

      expect(provider.getStatus().state).toBe('running');
      expect(spawnFn).toHaveBeenCalledTimes(2);
    });
  });

  describe('dispose', () => {
    it('kills the process', async () => {
      const { provider, healthCheckFn, fakeProcess } = createProvider();
      healthCheckFn.mockResolvedValue(true);

      const promise = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      await vi.advanceTimersByTimeAsync(2100);
      await promise;

      provider.dispose();
      expect(fakeProcess.kill).toHaveBeenCalledWith('SIGTERM');
    });

    it('is safe to call when already stopped', () => {
      const { provider } = createProvider();
      expect(() => provider.dispose()).not.toThrow();
    });
  });

  describe('tilde expansion', () => {
    it('expands ~ in venvPath', () => {
      const { spawnFn, healthCheckFn, provider } = createProvider({ venvPath: '~/vllm_env' });
      healthCheckFn.mockResolvedValue(true);

      // Trigger startup to inspect spawn args
      const promise = (provider as unknown as { ensureRunning(): Promise<void> }).ensureRunning();
      vi.advanceTimersByTimeAsync(2100).then(() => promise).catch(() => {});

      // The venvPath in spawn call should have ~ expanded
      if (spawnFn.mock.calls.length > 0) {
        const [bin] = spawnFn.mock.calls[0] as [string];
        expect(bin).not.toContain('~');
        expect(bin).toContain('/vllm_env/bin/vllm');
      }

      provider.dispose();
    });
  });
});
