import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import http from 'node:http';
import { Readable, Writable } from 'node:stream';
import { runMcpBridge, createMcpCommand } from '../../src/commands/mcp.js';

// ---- Mock MCP server (simulates mcplocal project endpoint) ----

interface RecordedRequest {
  method: string;
  url: string;
  headers: http.IncomingHttpHeaders;
  body: string;
}

let mockServer: http.Server;
let mockPort: number;
const recorded: RecordedRequest[] = [];
let sessionCounter = 0;

function makeInitializeResponse(id: number | string) {
  return JSON.stringify({
    jsonrpc: '2.0',
    id,
    result: {
      protocolVersion: '2024-11-05',
      capabilities: { tools: {} },
      serverInfo: { name: 'test-server', version: '1.0.0' },
    },
  });
}

function makeToolsListResponse(id: number | string) {
  return JSON.stringify({
    jsonrpc: '2.0',
    id,
    result: {
      tools: [
        { name: 'grafana/query', description: 'Query Grafana', inputSchema: { type: 'object', properties: {} } },
      ],
    },
  });
}

function makeToolCallResponse(id: number | string) {
  return JSON.stringify({
    jsonrpc: '2.0',
    id,
    result: {
      content: [{ type: 'text', text: 'tool result' }],
    },
  });
}

beforeAll(async () => {
  mockServer = http.createServer((req, res) => {
    const chunks: Buffer[] = [];
    req.on('data', (c: Buffer) => chunks.push(c));
    req.on('end', () => {
      const body = Buffer.concat(chunks).toString('utf-8');
      recorded.push({ method: req.method ?? '', url: req.url ?? '', headers: req.headers, body });

      if (req.method === 'DELETE') {
        res.writeHead(200);
        res.end();
        return;
      }

      if (req.method === 'POST' && req.url?.startsWith('/projects/')) {
        let sessionId = req.headers['mcp-session-id'] as string | undefined;

        // Assign session ID on first request
        if (!sessionId) {
          sessionCounter++;
          sessionId = `session-${sessionCounter}`;
        }
        res.setHeader('mcp-session-id', sessionId);

        // Parse JSON-RPC and respond based on method
        try {
          const rpc = JSON.parse(body) as { id: number | string; method: string };
          let responseBody: string;

          switch (rpc.method) {
            case 'initialize':
              responseBody = makeInitializeResponse(rpc.id);
              break;
            case 'tools/list':
              responseBody = makeToolsListResponse(rpc.id);
              break;
            case 'tools/call':
              responseBody = makeToolCallResponse(rpc.id);
              break;
            default:
              responseBody = JSON.stringify({ jsonrpc: '2.0', id: rpc.id, error: { code: -32601, message: 'Method not found' } });
          }

          // Respond in SSE format for /projects/sse-project/mcp
          if (req.url?.includes('sse-project')) {
            res.writeHead(200, { 'Content-Type': 'text/event-stream' });
            res.end(`event: message\ndata: ${responseBody}\n\n`);
          } else {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(responseBody);
          }
        } catch {
          res.writeHead(400, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: 'Invalid JSON' }));
        }
        return;
      }

      res.writeHead(404);
      res.end();
    });
  });

  await new Promise<void>((resolve) => {
    mockServer.listen(0, () => {
      const addr = mockServer.address();
      if (addr && typeof addr === 'object') {
        mockPort = addr.port;
      }
      resolve();
    });
  });
});

afterAll(() => {
  mockServer.close();
});

// ---- Helper to run bridge with mock streams ----

function createMockStreams() {
  const stdoutChunks: string[] = [];
  const stderrChunks: string[] = [];

  const stdout = new Writable({
    write(chunk: Buffer, _encoding, callback) {
      stdoutChunks.push(chunk.toString());
      callback();
    },
  });

  const stderr = new Writable({
    write(chunk: Buffer, _encoding, callback) {
      stderrChunks.push(chunk.toString());
      callback();
    },
  });

  return { stdout, stderr, stdoutChunks, stderrChunks };
}

function pushAndEnd(stdin: Readable, lines: string[]) {
  for (const line of lines) {
    stdin.push(line + '\n');
  }
  stdin.push(null); // EOF
}

// ---- Tests ----

describe('MCP STDIO Bridge', () => {
  beforeAll(() => {
    recorded.length = 0;
    sessionCounter = 0;
  });

  it('forwards initialize request and returns response', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout, stdoutChunks } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });

    pushAndEnd(stdin, [initMsg]);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    // Verify request was made to correct URL
    expect(recorded.some((r) => r.url === '/projects/test-project/mcp' && r.method === 'POST')).toBe(true);

    // Verify response on stdout
    const output = stdoutChunks.join('');
    const parsed = JSON.parse(output.trim());
    expect(parsed.result.serverInfo.name).toBe('test-server');
    expect(parsed.result.protocolVersion).toBe('2024-11-05');
  });

  it('sends session ID on subsequent requests', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout, stdoutChunks } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });
    const toolsListMsg = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });

    pushAndEnd(stdin, [initMsg, toolsListMsg]);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    // First POST should NOT have mcp-session-id header
    const firstPost = recorded.find((r) => r.method === 'POST' && r.body.includes('initialize'));
    expect(firstPost).toBeDefined();
    expect(firstPost!.headers['mcp-session-id']).toBeUndefined();

    // Second POST SHOULD have mcp-session-id header
    const secondPost = recorded.find((r) => r.method === 'POST' && r.body.includes('tools/list'));
    expect(secondPost).toBeDefined();
    expect(secondPost!.headers['mcp-session-id']).toMatch(/^session-/);

    // Verify tools/list response
    const lines = stdoutChunks.join('').trim().split('\n');
    expect(lines.length).toBe(2);
    const toolsResponse = JSON.parse(lines[1]);
    expect(toolsResponse.result.tools[0].name).toBe('grafana/query');
  });

  it('forwards tools/call and returns result', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout, stdoutChunks } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });
    const callMsg = JSON.stringify({
      jsonrpc: '2.0', id: 2, method: 'tools/call',
      params: { name: 'grafana/query', arguments: { query: 'test' } },
    });

    pushAndEnd(stdin, [initMsg, callMsg]);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    const lines = stdoutChunks.join('').trim().split('\n');
    expect(lines.length).toBe(2);
    const callResponse = JSON.parse(lines[1]);
    expect(callResponse.result.content[0].text).toBe('tool result');
  });

  it('forwards Authorization header when token provided', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });

    pushAndEnd(stdin, [initMsg]);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      token: 'my-secret-token',
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    const post = recorded.find((r) => r.method === 'POST');
    expect(post).toBeDefined();
    expect(post!.headers['authorization']).toBe('Bearer my-secret-token');
  });

  it('does not send Authorization header when no token', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });

    pushAndEnd(stdin, [initMsg]);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    const post = recorded.find((r) => r.method === 'POST');
    expect(post).toBeDefined();
    expect(post!.headers['authorization']).toBeUndefined();
  });

  it('sends DELETE to clean up session on stdin EOF', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });

    pushAndEnd(stdin, [initMsg]);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    // Should have a DELETE request for session cleanup
    const deleteReq = recorded.find((r) => r.method === 'DELETE');
    expect(deleteReq).toBeDefined();
    expect(deleteReq!.headers['mcp-session-id']).toMatch(/^session-/);
  });

  it('does not send DELETE if no session was established', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout } = createMockStreams();

    // Push EOF immediately with no messages
    stdin.push(null);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    expect(recorded.filter((r) => r.method === 'DELETE')).toHaveLength(0);
  });

  it('writes errors to stderr and sends JSON-RPC error to stdout', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout, stdoutChunks, stderr, stderrChunks } = createMockStreams();

    // Send to a non-existent port to trigger connection error
    const badMsg = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
    pushAndEnd(stdin, [badMsg]);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: 'http://localhost:1', // will fail to connect
      stdin, stdout, stderr,
    });

    // Error should be on stderr
    expect(stderrChunks.join('')).toContain('MCP bridge error');
    // stdout should contain a JSON-RPC error response so the client doesn't hang
    const out = stdoutChunks.join('');
    const parsed = JSON.parse(out.trim()) as { id: number; error: { code: number; message: string } };
    expect(parsed.id).toBe(1);
    expect(parsed.error.code).toBe(-32603);
    expect(parsed.error.message).toContain('Bridge error');
  });

  it('skips blank lines in stdin', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout, stdoutChunks } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });

    pushAndEnd(stdin, ['', '  ', initMsg, '']);

    await runMcpBridge({
      projectName: 'test-project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    // Only one POST (for the actual message)
    const posts = recorded.filter((r) => r.method === 'POST');
    expect(posts).toHaveLength(1);

    // One response line
    const lines = stdoutChunks.join('').trim().split('\n');
    expect(lines).toHaveLength(1);
  });

  it('handles SSE (text/event-stream) responses', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout, stdoutChunks } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });

    pushAndEnd(stdin, [initMsg]);

    await runMcpBridge({
      projectName: 'sse-project', // triggers SSE response from mock server
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
    });

    // Should extract JSON from SSE data: lines
    const output = stdoutChunks.join('').trim();
    const parsed = JSON.parse(output);
    expect(parsed.result.serverInfo.name).toBe('test-server');
  });

  it('URL-encodes project name', async () => {
    recorded.length = 0;
    const stdin = new Readable({ read() {} });
    const { stdout } = createMockStreams();
    const { stderr } = createMockStreams();

    const initMsg = JSON.stringify({
      jsonrpc: '2.0', id: 1, method: 'initialize',
      params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
    });

    pushAndEnd(stdin, [initMsg]);

    await runMcpBridge({
      projectName: 'my project',
      mcplocalUrl: `http://localhost:${mockPort}`,
      stdin, stdout, stderr,
    });

    const post = recorded.find((r) => r.method === 'POST');
    expect(post?.url).toBe('/projects/my%20project/mcp');
  });
});

describe('createMcpCommand', () => {
  it('accepts --project option directly', () => {
    const cmd = createMcpCommand({
      getProject: () => undefined,
      configLoader: () => ({ mcplocalUrl: 'http://localhost:3200' }),
      credentialsLoader: () => null,
    });
    const opt = cmd.options.find((o) => o.long === '--project');
    expect(opt).toBeDefined();
    expect(opt!.short).toBe('-p');
  });

  it('parses --project from command args', async () => {
    let capturedProject: string | undefined;
    const cmd = createMcpCommand({
      getProject: () => undefined,
      configLoader: () => ({ mcplocalUrl: `http://localhost:${mockPort}` }),
      credentialsLoader: () => null,
    });
    // Override the action to capture what project was parsed
    // We test by checking the option parsing works, not by running the full bridge
    const parsed = cmd.parse(['--project', 'test-proj'], { from: 'user' });
    capturedProject = parsed.opts().project;
    expect(capturedProject).toBe('test-proj');
  });

  it('parses -p shorthand from command args', () => {
    const cmd = createMcpCommand({
      getProject: () => undefined,
      configLoader: () => ({ mcplocalUrl: `http://localhost:${mockPort}` }),
      credentialsLoader: () => null,
    });
    const parsed = cmd.parse(['-p', 'my-project'], { from: 'user' });
    expect(parsed.opts().project).toBe('my-project');
  });
});
