import { EventEmitter } from 'node:events';
import { describe, it, expect, vi } from 'vitest';
import { WebSocket } from 'ws';
import { GatewayRuntimeCommandClient, TuiRuntimeGatewayBridge } from './tuiRuntimeGateway.js';

class FakeWebSocket extends EventEmitter {
  readyState: number = WebSocket.CONNECTING;
  sentPayloads: string[] = [];

  constructor() {
    super();
    queueMicrotask(() => {
      this.readyState = WebSocket.OPEN;
      this.emit('open');
    });
  }

  send(data: string, cb?: (error?: Error) => void): void {
    this.sentPayloads.push(data);
    cb?.();
    const payload = JSON.parse(data) as { id: number };
    this.emit('message', Buffer.from(JSON.stringify({
      id: payload.id,
      event: 'done',
      data: { content: 'Backend mode: config_default' },
    })));
  }

  close(code?: number, reason?: string): void {
    this.readyState = WebSocket.CLOSED;
    this.emit('close', code ?? 1000, Buffer.from(reason ?? ''));
  }
}

describe('GatewayRuntimeCommandClient', () => {
  it('sends /runtime command through agent.send stream and resolves done content', async () => {
    let createdSocket: FakeWebSocket | null = null;
    const client = new GatewayRuntimeCommandClient({
      url: 'ws://127.0.0.1:18800',
      websocketFactory: () => {
        const ws = new FakeWebSocket();
        createdSocket = ws;
        return ws as unknown as WebSocket;
      },
    });

    const output = await client.executeRuntimeCommand('status');

    expect(output).toBe('Backend mode: config_default');
    expect(createdSocket).not.toBeNull();
    const sent = JSON.parse(createdSocket!.sentPayloads[0]) as {
      method: string;
      params: { message: string; metadata: { command: string; commandArgs?: string } };
    };
    expect(sent.method).toBe('agent.send');
    expect(sent.params.message).toBe('/runtime status');
    expect(sent.params.metadata.command).toBe('runtime');
    expect(sent.params.metadata.commandArgs).toBe('status');
  });
});

describe('TuiRuntimeGatewayBridge', () => {
  it('auto-starts daemon runtime when initial gateway connect fails', async () => {
    const runtime = { shutdown: vi.fn(async () => {}) };
    const client = {
      connect: vi.fn()
        .mockRejectedValueOnce(new Error('connect ECONNREFUSED'))
        .mockResolvedValue(undefined),
      disconnect: vi.fn(),
      executeRuntimeCommand: vi.fn(async () => 'Backend mode: force_native'),
    };
    const startDaemon = vi.fn(async () => runtime);

    const bridge = new TuiRuntimeGatewayBridge({
      client,
      startDaemon,
      startupTimeoutMs: 500,
      retryIntervalMs: 1,
      sleep: async () => new Promise((resolve) => setTimeout(resolve, 1)),
    });

    const output = await bridge.executeRuntimeCommand('status');

    expect(startDaemon).toHaveBeenCalledOnce();
    expect(client.connect).toHaveBeenCalledTimes(2);
    expect(client.executeRuntimeCommand).toHaveBeenCalledWith('status');
    expect(output).toContain('force_native');

    await bridge.shutdown();
    expect(client.disconnect).toHaveBeenCalledOnce();
    expect(runtime.shutdown).toHaveBeenCalledOnce();
  });

  it('returns actionable error when gateway stays unavailable after auto-start', async () => {
    const client = {
      connect: vi.fn().mockRejectedValue(new Error('connect ECONNREFUSED')),
      disconnect: vi.fn(),
      executeRuntimeCommand: vi.fn(async () => ''),
    };
    const bridge = new TuiRuntimeGatewayBridge({
      client,
      startDaemon: vi.fn(async () => ({ shutdown: vi.fn(async () => {}) })),
      startupTimeoutMs: 10,
      retryIntervalMs: 1,
      sleep: async () => new Promise((resolve) => setTimeout(resolve, 1)),
    });

    await expect(bridge.ensureReady()).rejects.toThrow('Gateway did not become ready after auto-start');
  });

  it('treats EADDRINUSE during auto-start as an attach race and retries connect', async () => {
    const client = {
      connect: vi.fn()
        .mockRejectedValueOnce(new Error('connect ECONNREFUSED'))
        .mockResolvedValue(undefined),
      disconnect: vi.fn(),
      executeRuntimeCommand: vi.fn(async () => 'Backend mode: config_default'),
    };
    const startDaemon = vi.fn(async () => {
      const error = new Error('listen EADDRINUSE: address already in use 127.0.0.1:18800') as Error & { code?: string };
      error.code = 'EADDRINUSE';
      throw error;
    });
    const bridge = new TuiRuntimeGatewayBridge({
      client,
      startDaemon,
      startupTimeoutMs: 500,
      retryIntervalMs: 1,
      sleep: async () => new Promise((resolve) => setTimeout(resolve, 1)),
    });

    const output = await bridge.executeRuntimeCommand('status');

    expect(startDaemon).toHaveBeenCalledOnce();
    expect(client.connect).toHaveBeenCalledTimes(2);
    expect(output).toContain('config_default');
  });
});
