import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';

const TEST_PORT = 2525;

let testServer;
const TEST_TIMEOUT = 30000;

tap.test('prepare server', async () => {
  testServer = await startTestServer({ port: TEST_PORT });
  await new Promise(resolve => setTimeout(resolve, 100));
});

tap.test('Command Pipelining - should advertise PIPELINING in EHLO response', async (tools) => {
  const done = tools.defer();
  
  try {
    const socket = net.createConnection({
      host: 'localhost',
      port: TEST_PORT,
      timeout: TEST_TIMEOUT
    });
    
    await new Promise<void>((resolve, reject) => {
      socket.once('connect', () => resolve());
      socket.once('error', reject);
    });
    
    // Get banner
    const banner = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(banner).toInclude('220');
    
    // Send EHLO
    socket.write('EHLO testhost\r\n');
    
    const ehloResponse = await new Promise<string>((resolve) => {
      let data = '';
      const handler = (chunk: Buffer) => {
        data += chunk.toString();
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
          socket.removeListener('data', handler);
          resolve(data);
        }
      };
      socket.on('data', handler);
    });
    
    console.log('EHLO response:', ehloResponse);
    
    // Check if PIPELINING is advertised
    const pipeliningAdvertised = ehloResponse.includes('250-PIPELINING') || ehloResponse.includes('250 PIPELINING');
    console.log('PIPELINING advertised:', pipeliningAdvertised);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
    // Note: PIPELINING is optional per RFC 2920
    expect(ehloResponse).toInclude('250');
    
  } finally {
    done.resolve();
  }
});

tap.test('Command Pipelining - should handle pipelined MAIL FROM and RCPT TO', async (tools) => {
  const done = tools.defer();
  
  try {
    const socket = net.createConnection({
      host: 'localhost',
      port: TEST_PORT,
      timeout: TEST_TIMEOUT
    });
    
    await new Promise<void>((resolve, reject) => {
      socket.once('connect', () => resolve());
      socket.once('error', reject);
    });
    
    // Get banner
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Send EHLO
    socket.write('EHLO testhost\r\n');
    
    await new Promise<string>((resolve) => {
      let data = '';
      const handler = (chunk: Buffer) => {
        data += chunk.toString();
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
          socket.removeListener('data', handler);
          resolve(data);
        }
      };
      socket.on('data', handler);
    });
    
    // Send pipelined commands (all at once)
    const pipelinedCommands = 
      'MAIL FROM:<sender@example.com>\r\n' +
      'RCPT TO:<recipient@example.com>\r\n';
    
    console.log('Sending pipelined commands...');
    socket.write(pipelinedCommands);
    
    // Collect responses
    const responses = await new Promise<string>((resolve) => {
      let data = '';
      let responseCount = 0;
      const handler = (chunk: Buffer) => {
        data += chunk.toString();
        const lines = data.split('\r\n').filter(line => line.trim());
        
        // Count responses that look like complete SMTP responses
        const completeResponses = lines.filter(line => /^[0-9]{3}(\s|-)/.test(line));
        
        // We expect 2 responses (one for MAIL FROM, one for RCPT TO)
        if (completeResponses.length >= 2) {
          socket.removeListener('data', handler);
          resolve(data);
        }
      };
      socket.on('data', handler);
      
      // Timeout if we don't get responses
      setTimeout(() => {
        socket.removeListener('data', handler);
        resolve(data);
      }, 5000);
    });
    
    console.log('Pipelined command responses:', responses);
    
    // Parse responses
    const responseLines = responses.split('\r\n').filter(line => line.trim());
    const mailFromResponse = responseLines.find(line => line.match(/^250.*/) && responseLines.indexOf(line) === 0);
    const rcptToResponse = responseLines.find(line => line.match(/^250.*/) && responseLines.indexOf(line) === 1);
    
    // Both commands should succeed
    expect(mailFromResponse).toBeDefined();
    expect(rcptToResponse).toBeDefined();
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Command Pipelining - should handle pipelined commands with DATA', async (tools) => {
  const done = tools.defer();
  
  try {
    const socket = net.createConnection({
      host: 'localhost',
      port: TEST_PORT,
      timeout: TEST_TIMEOUT
    });
    
    await new Promise<void>((resolve, reject) => {
      socket.once('connect', () => resolve());
      socket.once('error', reject);
    });
    
    // Get banner
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Send EHLO
    socket.write('EHLO testhost\r\n');
    
    await new Promise<string>((resolve) => {
      let data = '';
      const handler = (chunk: Buffer) => {
        data += chunk.toString();
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
          socket.removeListener('data', handler);
          resolve(data);
        }
      };
      socket.on('data', handler);
    });
    
    // Send pipelined MAIL FROM, RCPT TO, and DATA commands
    const pipelinedCommands = 
      'MAIL FROM:<sender@example.com>\r\n' +
      'RCPT TO:<recipient@example.com>\r\n' +
      'DATA\r\n';
    
    console.log('Sending pipelined commands with DATA...');
    socket.write(pipelinedCommands);
    
    // Collect responses
    const responses = await new Promise<string>((resolve) => {
      let data = '';
      const handler = (chunk: Buffer) => {
        data += chunk.toString();
        
        // Look for the DATA prompt (354)
        if (data.includes('354')) {
          socket.removeListener('data', handler);
          resolve(data);
        }
      };
      socket.on('data', handler);
      
      setTimeout(() => {
        socket.removeListener('data', handler);
        resolve(data);
      }, 5000);
    });
    
    console.log('Responses including DATA:', responses);
    
    // Should get 250 for MAIL FROM, 250 for RCPT TO, and 354 for DATA
    expect(responses).toInclude('250'); // MAIL FROM OK
    expect(responses).toInclude('354'); // Start mail input
    
    // Send email content
    const emailContent = 'Subject: Pipelining Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\nTest email with pipelining.\r\n.\r\n';
    socket.write(emailContent);
    
    // Get final response
    const finalResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Final response:', finalResponse);
    expect(finalResponse).toInclude('250');
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Command Pipelining - should handle pipelined NOOP commands', async (tools) => {
  const done = tools.defer();
  
  try {
    const socket = net.createConnection({
      host: 'localhost',
      port: TEST_PORT,
      timeout: TEST_TIMEOUT
    });
    
    await new Promise<void>((resolve, reject) => {
      socket.once('connect', () => resolve());
      socket.once('error', reject);
    });
    
    // Get banner
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Send EHLO
    socket.write('EHLO testhost\r\n');
    
    await new Promise<string>((resolve) => {
      let data = '';
      const handler = (chunk: Buffer) => {
        data += chunk.toString();
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
          socket.removeListener('data', handler);
          resolve(data);
        }
      };
      socket.on('data', handler);
    });
    
    // Send multiple pipelined NOOP commands
    const pipelinedNoops = 
      'NOOP\r\n' +
      'NOOP\r\n' +
      'NOOP\r\n';
    
    console.log('Sending pipelined NOOP commands...');
    socket.write(pipelinedNoops);
    
    // Collect responses
    const responses = await new Promise<string>((resolve) => {
      let data = '';
      const handler = (chunk: Buffer) => {
        data += chunk.toString();
        const responseCount = (data.match(/^250.*OK/gm) || []).length;
        
        // We expect 3 NOOP responses
        if (responseCount >= 3) {
          socket.removeListener('data', handler);
          resolve(data);
        }
      };
      socket.on('data', handler);
      
      setTimeout(() => {
        socket.removeListener('data', handler);
        resolve(data);
      }, 5000);
    });
    
    console.log('NOOP responses:', responses);
    
    // Count OK responses
    const okResponses = (responses.match(/^250.*OK/gm) || []).length;
    expect(okResponses).toBeGreaterThanOrEqual(3);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('cleanup server', async () => {
  await stopTestServer(testServer);
});

export default tap.start();