import { afterEach, describe, expect, it } from 'bun:test';
import { spawn } from 'child_process';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import * as http from 'node:http';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const serverPath = join(process.cwd(), 'lib', 'mcp', 'ccs-image-analysis-server.cjs');

function encodeMessage(message: unknown): string {
  return `${JSON.stringify(message)}\n`;
}

function encodeLegacyMessage(message: unknown): string {
  const body = JSON.stringify(message);
  return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
}

function collectResponses(
  child: ReturnType<typeof spawn>,
  expectedCount: number
): Promise<Array<Record<string, unknown>>> {
  return new Promise((resolve, reject) => {
    let buffer = Buffer.alloc(0);
    const responses: Array<Record<string, unknown>> = [];
    const timer = setTimeout(() => reject(new Error('Timed out waiting for MCP responses')), 5000);

    function tryParse(): void {
      while (true) {
        const newlineIndex = buffer.indexOf('\n');
        if (newlineIndex === -1) {
          return;
        }

        const body = buffer.slice(0, newlineIndex).toString('utf8').replace(/\r$/, '').trim();
        buffer = buffer.slice(newlineIndex + 1);
        if (!body) {
          continue;
        }

        responses.push(JSON.parse(body) as Record<string, unknown>);
        if (responses.length >= expectedCount) {
          clearTimeout(timer);
          resolve(responses);
          return;
        }
      }
    }

    child.stdout.on('data', (chunk: Buffer) => {
      buffer = Buffer.concat([buffer, chunk]);
      try {
        tryParse();
      } catch (error) {
        clearTimeout(timer);
        reject(error);
      }
    });

    child.on('error', (error) => {
      clearTimeout(timer);
      reject(error);
    });
  });
}

function createTestPng(filePath: string): void {
  const png = Buffer.from([
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
    0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
    0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8,
    0xcf, 0xc0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb4, 0x00, 0x00, 0x00,
    0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
  ]);
  writeFileSync(filePath, png);
}

function createTestPdf(filePath: string): void {
  writeFileSync(filePath, '%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF\n', 'utf8');
}

describe('ccs-image-analysis MCP server', () => {
  let tempDir = '';
  let imagePath = '';
  let mockServer: http.Server | null = null;

  afterEach(async () => {
    await new Promise<void>((resolve) => {
      mockServer?.close(() => resolve());
      if (!mockServer) resolve();
    });
    mockServer = null;

    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
      imagePath = '';
    }
  });

  it('lists the ImageAnalysis tool and posts directly to the provider-scoped CLIProxy route', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-mcp-server-'));
    imagePath = join(tempDir, 'screen.png');
    createTestPng(imagePath);

    let receivedRequest: { path?: string; apiKey?: string; body?: unknown } | null = null;
    const mockPort = await new Promise<number>((resolve, reject) => {
      mockServer = http.createServer((req, res) => {
        let body = '';
        req.on('data', (chunk) => {
          body += chunk.toString();
        });
        req.on('end', () => {
          receivedRequest = {
            path: req.url || '/',
            apiKey: req.headers['x-api-key'] as string,
            body: body ? JSON.parse(body) : null,
          };

          res.writeHead(200, { 'Content-Type': 'application/json' });
          res.end(
            JSON.stringify({
              content: [{ type: 'text', text: 'The screenshot shows a red pixel debug fixture.' }],
            })
          );
        });
      });

      mockServer.once('error', reject);
      mockServer.listen(0, '127.0.0.1', () => {
        const address = mockServer?.address();
        if (!address || typeof address === 'string') {
          reject(new Error('Failed to resolve mock server port'));
          return;
        }
        resolve(address.port);
      });
    });

    const child = spawn('node', [serverPath], {
      cwd: tempDir,
      env: {
        ...process.env,
        CCS_IMAGE_ANALYSIS_ENABLED: '1',
        CCS_IMAGE_ANALYSIS_SKIP: '0',
        CCS_CURRENT_PROVIDER: 'agy',
        CCS_IMAGE_ANALYSIS_MODEL: 'gemini-3-1-flash-preview',
        CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: `http://127.0.0.1:${mockPort}`,
        CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '/api/provider/agy',
        CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY: 'test-api-key',
      },
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    try {
      const responsesPromise = collectResponses(child, 3);
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 1,
          method: 'initialize',
          params: {
            protocolVersion: '2024-11-05',
            capabilities: {},
            clientInfo: { name: 'bun-test', version: '1.0.0' },
          },
        })
      );
      child.stdin.write(encodeMessage({ jsonrpc: '2.0', id: 2, method: 'tools/list' }));
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: {
            name: 'ImageAnalysis',
            arguments: { filePath: imagePath, focus: 'Describe the visible issue' },
          },
        })
      );

      const responses = await responsesPromise;
      const toolsList = responses.find((message) => message.id === 2);
      const toolCall = responses.find((message) => message.id === 3);

      expect(toolsList?.result).toEqual({
        tools: [
          {
            name: 'ImageAnalysis',
            description:
              'Analyze a local image or PDF file with CCS provider-backed vision. Prefer this tool over Read for image and PDF paths. Use Read for text, code, and other plain files.',
            inputSchema: {
              type: 'object',
              properties: {
                filePath: {
                  type: 'string',
                  description:
                    'Workspace-relative path, or an absolute path inside the current workspace, to a local image or PDF file to analyze.',
                },
                focus: {
                  type: 'string',
                  description:
                    'Optional question or area of focus, for example "explain the error dialog" or "transcribe the visible text".',
                },
                template: {
                  type: 'string',
                  enum: ['default', 'screenshot', 'document'],
                  description:
                    'Optional prompt template override. Use screenshot for UI captures, document for PDFs/docs, or default for general images.',
                },
              },
              required: ['filePath'],
              additionalProperties: false,
            },
          },
        ],
      });
      expect(receivedRequest?.path).toBe('/api/provider/agy/v1/messages');
      expect(receivedRequest?.apiKey).toBe('test-api-key');
      expect(
        (((receivedRequest?.body as { messages: Array<{ content: Array<{ text?: string }> }> })
          ?.messages[0]?.content[0] || {}) as { text?: string }).text
      ).toContain('Specific focus');
      expect(toolCall?.result).toBeDefined();
      expect(
        ((toolCall?.result as { content: Array<{ text: string }> }).content[0] || {}).text
      ).toContain('red pixel debug fixture');
      expect(
        ((toolCall?.result as { content: Array<{ text: string }> }).content[0] || {}).text
      ).toContain('Model: gemini-3-1-flash-preview');
    } finally {
      child.kill();
    }
  });

  it('returns a structured tool error when the file does not exist', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-mcp-server-missing-'));
    const missingPath = join(tempDir, 'missing-image.png');
    const child = spawn('node', [serverPath], {
      cwd: tempDir,
      env: {
        ...process.env,
        CCS_IMAGE_ANALYSIS_ENABLED: '1',
        CCS_IMAGE_ANALYSIS_SKIP: '0',
        CCS_CURRENT_PROVIDER: 'agy',
        CCS_IMAGE_ANALYSIS_MODEL: 'gemini-3-1-flash-preview',
      },
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    try {
      const responsesPromise = collectResponses(child, 2);
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 1,
          method: 'initialize',
          params: {
            protocolVersion: '2024-11-05',
            capabilities: {},
            clientInfo: { name: 'bun-test', version: '1.0.0' },
          },
        })
      );
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'ImageAnalysis',
            arguments: { filePath: missingPath },
          },
        })
      );

      const responses = await responsesPromise;
      const toolCall = responses.find((message) => message.id === 2);

      expect(toolCall?.result).toEqual({
        content: [
          {
            type: 'text',
            text: `ImageAnalysis could not find file: ${missingPath}`,
          },
        ],
        isError: true,
      });
    } finally {
      child.kill();
    }
  });

  it('rejects paths outside the current workspace', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-mcp-server-scope-'));
    const workspaceDir = join(tempDir, 'workspace');
    mkdirSync(workspaceDir, { recursive: true });
    const outsidePath = join(tempDir, 'outside.png');
    createTestPng(outsidePath);

    const child = spawn('node', [serverPath], {
      cwd: workspaceDir,
      env: {
        ...process.env,
        CCS_IMAGE_ANALYSIS_ENABLED: '1',
        CCS_IMAGE_ANALYSIS_SKIP: '0',
        CCS_CURRENT_PROVIDER: 'agy',
        CCS_IMAGE_ANALYSIS_MODEL: 'gemini-3-1-flash-preview',
      },
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    try {
      const responsesPromise = collectResponses(child, 2);
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 1,
          method: 'initialize',
          params: {
            protocolVersion: '2024-11-05',
            capabilities: {},
            clientInfo: { name: 'bun-test', version: '1.0.0' },
          },
        })
      );
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'ImageAnalysis',
            arguments: { filePath: '../outside.png' },
          },
        })
      );

      const responses = await responsesPromise;
      const toolCall = responses.find((message) => message.id === 2);
      expect(toolCall?.error).toEqual({
        code: -32602,
        message: 'ImageAnalysis only allows files inside the current workspace.',
      });
    } finally {
      child.kill();
    }
  });

  it('sends PDF files as document blocks to the provider-scoped route', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-mcp-server-pdf-'));
    const pdfPath = join(tempDir, 'manual.pdf');
    createTestPdf(pdfPath);

    let receivedRequest: { path?: string; apiKey?: string; body?: unknown } | null = null;
    const mockPort = await new Promise<number>((resolve, reject) => {
      mockServer = http.createServer((req, res) => {
        let body = '';
        req.on('data', (chunk) => {
          body += chunk.toString();
        });
        req.on('end', () => {
          receivedRequest = {
            path: req.url || '/',
            apiKey: req.headers['x-api-key'] as string,
            body: body ? JSON.parse(body) : null,
          };

          res.writeHead(200, { 'Content-Type': 'application/json' });
          res.end(
            JSON.stringify({
              content: [{ type: 'text', text: 'The PDF contains a one-page fixture.' }],
            })
          );
        });
      });

      mockServer.once('error', reject);
      mockServer.listen(0, '127.0.0.1', () => {
        const address = mockServer?.address();
        if (!address || typeof address === 'string') {
          reject(new Error('Failed to resolve mock server port'));
          return;
        }
        resolve(address.port);
      });
    });

    const child = spawn('node', [serverPath], {
      cwd: tempDir,
      env: {
        ...process.env,
        CCS_IMAGE_ANALYSIS_ENABLED: '1',
        CCS_IMAGE_ANALYSIS_SKIP: '0',
        CCS_CURRENT_PROVIDER: 'agy',
        CCS_IMAGE_ANALYSIS_MODEL: 'gemini-3-1-flash-preview',
        CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: `http://127.0.0.1:${mockPort}`,
        CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '/api/provider/agy',
        CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY: 'test-api-key',
      },
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    try {
      const responsesPromise = collectResponses(child, 2);
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 1,
          method: 'initialize',
          params: {
            protocolVersion: '2024-11-05',
            capabilities: {},
            clientInfo: { name: 'bun-test', version: '1.0.0' },
          },
        })
      );
      child.stdin.write(
        encodeMessage({
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'ImageAnalysis',
            arguments: { filePath: pdfPath },
          },
        })
      );

      const responses = await responsesPromise;
      const requestBody = receivedRequest?.body as {
        messages: Array<{ content: Array<{ type: string; source?: { media_type?: string } }> }>;
      };

      expect(receivedRequest?.path).toBe('/api/provider/agy/v1/messages');
      expect(receivedRequest?.apiKey).toBe('test-api-key');
      expect(requestBody.messages[0]?.content[1]?.type).toBe('document');
      expect(requestBody.messages[0]?.content[1]?.source?.media_type).toBe('application/pdf');
      expect(((responses[1]?.result as { content: Array<{ text: string }> }).content[0] || {}).text).toContain(
        'one-page fixture'
      );
    } finally {
      child.kill();
    }
  });

  it('accepts legacy Content-Length framed requests for compatibility', async () => {
    const child = spawn('node', [serverPath], {
      env: {
        ...process.env,
        CCS_IMAGE_ANALYSIS_ENABLED: '1',
        CCS_IMAGE_ANALYSIS_SKIP: '1',
        CCS_CURRENT_PROVIDER: 'agy',
      },
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    try {
      const responsesPromise = collectResponses(child, 2);
      child.stdin.write(
        encodeLegacyMessage({
          jsonrpc: '2.0',
          id: 1,
          method: 'initialize',
          params: {
            protocolVersion: '2024-11-05',
            capabilities: {},
            clientInfo: { name: 'bun-test', version: '1.0.0' },
          },
        })
      );
      child.stdin.write(encodeLegacyMessage({ jsonrpc: '2.0', id: 2, method: 'tools/list' }));

      const responses = await responsesPromise;
      const toolsList = responses.find((message) => message.id === 2);
      expect(toolsList?.result).toEqual({ tools: [] });
    } finally {
      child.kill();
    }
  });
});
