import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import { WebSocket } from 'ws';
import { resolve } from 'path';
import { createServer } from 'net';
import { GatewayServer } from './server.js';
import type { GatewayServerConfig } from './server.js';
import type { GatewayResponse, GatewayError, GatewayEvent } from './protocol.js';
import { ErrorCode } from './protocol.js';

async function canListenOnLocalhost(): Promise<boolean> {
  return await new Promise((resolvePromise) => {
    const s = createServer();
    s.once('error', () => resolvePromise(false));
    s.listen(0, '127.0.0.1', () => {
      s.close(() => resolvePromise(true));
    });
  });
}

let LISTEN_ALLOWED = true;

// Minimal mocks for dependencies
const mockSession = {
  id: 'test',
  addMessage: vi.fn(),
  getHistory: vi.fn(() => []),
  clear: vi.fn(),
  setHistory: vi.fn(),
  replaceHistory: vi.fn(),
};

const mockSessionManager = {
  getSession: vi.fn(() => mockSession),
  listSessions: vi.fn(() => ['ws:test']),
  transferSession: vi.fn(),
  closeSession: vi.fn(),
};

const mockModelClient = {
  chat: vi.fn(async () => ({
    content: 'Hello from Flynn!',
    stopReason: 'end_turn',
    usage: { inputTokens: 10, outputTokens: 5 },
  })),
};

const mockToolRegistry = {
  register: vi.fn(),
  get: vi.fn((name: string) => (name === 'shell.exec' ? { name: 'shell.exec', description: 'Run shell', inputSchema: { type: 'object', properties: {} } } : undefined)),
  list: vi.fn(() => [{ name: 'shell.exec', description: 'Run shell', inputSchema: { type: 'object', properties: {} } }]),
  filteredList: vi.fn(() => [{ name: 'shell.exec', description: 'Run shell', inputSchema: { type: 'object', properties: {} } }]),
  toAnthropicFormat: vi.fn(() => []),
  toOpenAIFormat: vi.fn(() => []),
  filteredToAnthropicFormat: vi.fn(() => []),
  filteredToOpenAIFormat: vi.fn(() => []),
};

const mockToolExecutor = {
  execute: vi.fn(async () => ({ success: true, output: 'executed' })),
};

const TEST_PORT = 18899;

let server: GatewayServer;

function createClient(): Promise<WebSocket> {
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(`ws://127.0.0.1:${TEST_PORT}`);
    ws.on('open', () => resolve(ws));
    ws.on('error', reject);
  });
}

function sendAndReceive(ws: WebSocket, msg: object): Promise<GatewayResponse | GatewayError | GatewayEvent> {
  return new Promise((resolve) => {
    ws.once('message', (data) => {
      resolve(JSON.parse(data.toString()));
    });
    ws.send(JSON.stringify(msg));
  });
}

function sendAndReceiveAll(ws: WebSocket, msg: object, count: number): Promise<Array<GatewayResponse | GatewayError | GatewayEvent>> {
  return new Promise((resolve) => {
    const messages: Array<GatewayResponse | GatewayError | GatewayEvent> = [];
    const handler = (data: Buffer) => {
      messages.push(JSON.parse(data.toString()));
      if (messages.length >= count) {
        ws.off('message', handler);
        resolve(messages);
      }
    };
    ws.on('message', handler);
    ws.send(JSON.stringify(msg));
  });
}

beforeAll(async () => {
  LISTEN_ALLOWED = await canListenOnLocalhost();
});

describe('GatewayServer integration', () => {
  beforeAll(async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    server = new GatewayServer({
      port: TEST_PORT,
      sessionManager: mockSessionManager as unknown as GatewayServerConfig['sessionManager'],
      modelClient: mockModelClient,
      systemPrompt: 'Test prompt',
      toolRegistry: mockToolRegistry as unknown as GatewayServerConfig['toolRegistry'],
      toolExecutor: mockToolExecutor as unknown as GatewayServerConfig['toolExecutor'],
      version: '0.1.0-test',
      uiDir: resolve(import.meta.dirname, 'ui'),
    });
    await server.start();
  });

  afterAll(async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    await server.stop();
  });

  it('responds to system.health', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws = await createClient();
    try {
      const result = await sendAndReceive(ws, { id: 1, method: 'system.health' });
      const response = result as GatewayResponse;
      expect(response.id).toBe(1);
      const r = response.result as Record<string, unknown>;
      expect(r.status).toBe('ok');
      expect(r.version).toBe('0.1.0-test');
      expect(typeof r.uptime).toBe('number');
    } finally {
      ws.close();
    }
  });

  it('returns MethodNotFound for unknown method', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws = await createClient();
    try {
      const result = await sendAndReceive(ws, { id: 2, method: 'unknown.method' });
      const error = result as GatewayError;
      expect(error.error.code).toBe(ErrorCode.MethodNotFound);
    } finally {
      ws.close();
    }
  });

  it('returns ParseError for invalid JSON', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws = await createClient();
    try {
      const result = await new Promise<GatewayError>((resolve) => {
        ws.once('message', (data) => resolve(JSON.parse(data.toString())));
        ws.send('not valid json');
      });
      expect(result.error.code).toBe(ErrorCode.ParseError);
    } finally {
      ws.close();
    }
  });

  it('lists tools via tools.list', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws = await createClient();
    try {
      const result = await sendAndReceive(ws, { id: 3, method: 'tools.list' });
      const response = result as GatewayResponse;
      const r = response.result as { tools: Array<{ name: string }> };
      expect(r.tools.length).toBeGreaterThan(0);
      expect(r.tools[0].name).toBe('shell.exec');
    } finally {
      ws.close();
    }
  });

  it('sends agent message and receives done event', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws = await createClient();
    try {
      // agent.send streams events — we expect a 'done' event
      const messages = await sendAndReceiveAll(ws, { id: 4, method: 'agent.send', params: { message: 'hi' } }, 1);
      const doneEvent = messages[0] as GatewayEvent;
      expect(doneEvent.id).toBe(4);
      expect(doneEvent.event).toBe('done');
      expect((doneEvent.data as any).content).toBe('Hello from Flynn!');
    } finally {
      ws.close();
    }
  });

  it('tracks connections correctly', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws1 = await createClient();
    const ws2 = await createClient();
    try {
      const result = await sendAndReceive(ws1, { id: 5, method: 'system.health' });
      const r = (result as GatewayResponse).result as Record<string, unknown>;
      expect(r.connections).toBe(2);
    } finally {
      ws1.close();
      ws2.close();
    }
  });

  it('lists registered methods', () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const methods = server.getMethods();
    expect(methods).toContain('system.health');
    expect(methods).toContain('agent.send');
    expect(methods).toContain('agent.cancel');
    expect(methods).toContain('sessions.list');
    expect(methods).toContain('sessions.history');
    expect(methods).toContain('sessions.create');
    expect(methods).toContain('tools.list');
    expect(methods).toContain('tools.invoke');
  });

  // ── HTTP static file serving tests ────────────────────────────

  it('serves index.html on HTTP GET /', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const res = await fetch(`http://127.0.0.1:${TEST_PORT}/`);
    expect(res.status).toBe(200);
    expect(res.headers.get('content-type')).toBe('text/html');
    const body = await res.text();
    expect(body).toContain('Flynn');
  });

  it('serves style.css on HTTP GET /style.css', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const res = await fetch(`http://127.0.0.1:${TEST_PORT}/style.css`);
    expect(res.status).toBe(200);
    expect(res.headers.get('content-type')).toBe('text/css');
  });

  it('returns 404 for unknown HTTP path', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const res = await fetch(`http://127.0.0.1:${TEST_PORT}/nonexistent`);
    expect(res.status).toBe(404);
  });

  it('returns 404 for path traversal attempt', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const res = await fetch(`http://127.0.0.1:${TEST_PORT}/../../../etc/passwd`);
    expect(res.status).toBe(404);
  });
});

describe('GatewayServer lock mode', () => {
  const LOCK_PORT = 18897;
  let lockServer: GatewayServer;

  beforeAll(async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    lockServer = new GatewayServer({
      port: LOCK_PORT,
      sessionManager: mockSessionManager as unknown as GatewayServerConfig['sessionManager'],
      modelClient: mockModelClient,
      systemPrompt: 'Test prompt',
      toolRegistry: mockToolRegistry as unknown as GatewayServerConfig['toolRegistry'],
      toolExecutor: mockToolExecutor as unknown as GatewayServerConfig['toolExecutor'],
      lock: true,
      uiDir: resolve(import.meta.dirname, 'ui'),
    });
    await lockServer.start();
  });

  afterAll(async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    await lockServer.stop();
  });

  function createLockClient(): Promise<WebSocket> {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(`ws://127.0.0.1:${LOCK_PORT}`);
      ws.on('open', () => resolve(ws));
      ws.on('error', reject);
    });
  }

  it('allows the first client to connect', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws = await createLockClient();
    try {
      const result = await sendAndReceive(ws, { id: 1, method: 'system.health' });
      const response = result as GatewayResponse;
      expect((response.result as any).status).toBe('ok');
    } finally {
      ws.close();
      // Wait for the close to propagate so connectionMap is empty
      await new Promise(r => setTimeout(r, 100));
    }
  });

  it('rejects second client with code 4003 when locked', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws1 = await createLockClient();
    try {
      // Second client should be rejected
      const closePromise = new Promise<{ code: number; reason: string }>((resolve) => {
        const ws2 = new WebSocket(`ws://127.0.0.1:${LOCK_PORT}`);
        ws2.on('close', (code, reason) => {
          resolve({ code, reason: reason.toString() });
        });
      });

      const { code, reason } = await closePromise;
      expect(code).toBe(4003);
      expect(reason).toContain('locked');
    } finally {
      ws1.close();
      await new Promise(r => setTimeout(r, 100));
    }
  });

  it('allows a new client after the previous one disconnects', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws1 = await createLockClient();
    ws1.close();
    // Wait for the close to propagate
    await new Promise(r => setTimeout(r, 100));

    const ws2 = await createLockClient();
    try {
      const result = await sendAndReceive(ws2, { id: 2, method: 'system.health' });
      const response = result as GatewayResponse;
      expect((response.result as any).status).toBe('ok');
    } finally {
      ws2.close();
      await new Promise(r => setTimeout(r, 100));
    }
  });

  it('system.lock handler returns lock status', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const ws = await createLockClient();
    try {
      const result = await sendAndReceive(ws, { id: 3, method: 'system.lock' });
      const response = result as GatewayResponse;
      const r = response.result as { locked: boolean; activeClients: number; maxClients: number | null };
      expect(r.locked).toBe(true);
      expect(r.activeClients).toBe(1);
      expect(r.maxClients).toBe(1);
    } finally {
      ws.close();
      await new Promise(r => setTimeout(r, 100));
    }
  });
});

describe('GatewayServer HTTP auth', () => {
  const AUTH_PORT = 18898;
  let authServer: GatewayServer;

  beforeAll(async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    authServer = new GatewayServer({
      port: AUTH_PORT,
      sessionManager: mockSessionManager as unknown as GatewayServerConfig['sessionManager'],
      modelClient: mockModelClient,
      systemPrompt: 'Test prompt',
      toolRegistry: mockToolRegistry as unknown as GatewayServerConfig['toolRegistry'],
      toolExecutor: mockToolExecutor as unknown as GatewayServerConfig['toolExecutor'],
      auth: { token: 'test-secret' },
      authHttp: true,
      uiDir: resolve(import.meta.dirname, 'ui'),
    });
    await authServer.start();
  });

  afterAll(async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    await authServer.stop();
  });

  it('returns 401 for HTTP request without token', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const res = await fetch(`http://127.0.0.1:${AUTH_PORT}/`);
    expect(res.status).toBe(401);
    expect(res.headers.get('www-authenticate')).toBe('Bearer');
  });

  it('serves content with valid Bearer token', async () => {
    if (!LISTEN_ALLOWED) {
      return;
    }
    const res = await fetch(`http://127.0.0.1:${AUTH_PORT}/`, {
      headers: { Authorization: 'Bearer test-secret' },
    });
    expect(res.status).toBe(200);
    expect(res.headers.get('content-type')).toBe('text/html');
  });
});
