import { describe, it, expect, afterAll, afterEach } from 'vitest';
import http from 'node:http';
import { McpdClient, ConnectionError } from '../src/http/mcpd-client.js';

/**
 * Create a local HTTP server for testing McpdClient behavior.
 * Returns the server and its URL.
 */
function createTestServer(
  handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
): Promise<{ server: http.Server; url: string }> {
  return new Promise((resolve) => {
    const server = http.createServer(handler);
    server.listen(0, '127.0.0.1', () => {
      const addr = server.address() as { port: number };
      resolve({ server, url: `http://127.0.0.1:${addr.port}` });
    });
  });
}

describe('McpdClient', () => {
  const servers: http.Server[] = [];

  afterEach(() => {
    for (const s of servers) s.close();
    servers.length = 0;
  });

  afterAll(() => {
    for (const s of servers) s.close();
  });

  it('makes GET requests with auth header', async () => {
    let capturedAuth = '';
    const { server, url } = await createTestServer((req, res) => {
      capturedAuth = req.headers['authorization'] ?? '';
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: true }));
    });
    servers.push(server);

    const client = new McpdClient(url, 'my-token');
    const result = await client.get<{ ok: boolean }>('/api/v1/test');

    expect(result).toEqual({ ok: true });
    expect(capturedAuth).toBe('Bearer my-token');
  });

  it('makes POST requests with JSON body', async () => {
    let capturedBody = '';
    const { server, url } = await createTestServer((req, res) => {
      const chunks: Buffer[] = [];
      req.on('data', (c: Buffer) => chunks.push(c));
      req.on('end', () => {
        capturedBody = Buffer.concat(chunks).toString();
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ received: true }));
      });
    });
    servers.push(server);

    const client = new McpdClient(url, 'tok');
    const result = await client.post<{ received: boolean }>('/api/v1/proxy', { serverId: 's1' });

    expect(result).toEqual({ received: true });
    expect(JSON.parse(capturedBody)).toEqual({ serverId: 's1' });
  });

  it('throws ConnectionError on connection refused', async () => {
    const client = new McpdClient('http://127.0.0.1:1', 'tok');

    await expect(client.get('/test')).rejects.toThrow(ConnectionError);
  });

  it('throws on 4xx/5xx responses', async () => {
    const { server, url } = await createTestServer((_req, res) => {
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'internal' }));
    });
    servers.push(server);

    const client = new McpdClient(url, 'tok');
    await expect(client.get('/test')).rejects.toThrow(/mcpd returned 500/);
  });

  // ── Timeout behavior ──

  it('times out on slow responses and throws ConnectionError', async () => {
    const { server, url } = await createTestServer((_req, _res) => {
      // Never respond — simulates a hanging upstream tool call
    });
    servers.push(server);

    // Use a very short timeout for the test
    const client = new McpdClient(url, 'tok', undefined, 500);

    const start = Date.now();
    await expect(client.post('/api/v1/mcp/proxy', { serverId: 's1' })).rejects.toThrow(
      /timed out/,
    );
    const elapsed = Date.now() - start;

    // Should have timed out around 500ms, not hung for seconds
    expect(elapsed).toBeGreaterThanOrEqual(450);
    expect(elapsed).toBeLessThan(3000);
  });

  it('timeout error is a ConnectionError with descriptive message', async () => {
    const { server, url } = await createTestServer((_req, _res) => {
      // Never respond
    });
    servers.push(server);

    const client = new McpdClient(url, 'tok', undefined, 200);

    try {
      await client.get('/test');
      expect.unreachable('Should have thrown');
    } catch (err) {
      expect(err).toBeInstanceOf(ConnectionError);
      expect((err as Error).message).toContain('Request timed out after 200ms');
    }
  });

  it('fast responses succeed within the timeout window', async () => {
    const { server, url } = await createTestServer((_req, res) => {
      // Respond immediately
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ fast: true }));
    });
    servers.push(server);

    // Short timeout, but response is immediate — should work
    const client = new McpdClient(url, 'tok', undefined, 500);
    const result = await client.get<{ fast: boolean }>('/test');
    expect(result).toEqual({ fast: true });
  });

  it('withHeaders preserves timeout', async () => {
    const { server, url } = await createTestServer((_req, _res) => {
      // Never respond
    });
    servers.push(server);

    const client = new McpdClient(url, 'tok', undefined, 300);
    const derived = client.withHeaders({ 'X-Custom': 'val' });

    const start = Date.now();
    await expect(derived.get('/test')).rejects.toThrow(/timed out/);
    const elapsed = Date.now() - start;
    expect(elapsed).toBeLessThan(2000);
  });

  it('default timeout is 30 seconds', async () => {
    // We can't wait 30s in a test, but we can verify the error message format
    // when a custom timeout is not set. Use a fast-failing server instead.
    const { server, url } = await createTestServer((_req, res) => {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: true }));
    });
    servers.push(server);

    // Default constructor — should work for fast responses
    const client = new McpdClient(url, 'tok');
    const result = await client.get<{ ok: boolean }>('/test');
    expect(result).toEqual({ ok: true });
  });
});
