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 = 15000;

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

tap.test('DATA - should accept email data after RCPT TO', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  
  socket.on('data', (data) => {
    receivedData += data.toString();
    
    if (currentStep === 'connecting' && receivedData.includes('220')) {
      currentStep = 'ehlo';
      receivedData = '';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
      currentStep = 'mail_from';
      receivedData = '';
      socket.write('MAIL FROM:<sender@example.com>\r\n');
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
      currentStep = 'rcpt_to';
      receivedData = '';
      socket.write('RCPT TO:<recipient@example.com>\r\n');
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
      currentStep = 'data_command';
      receivedData = '';
      socket.write('DATA\r\n');
    } else if (currentStep === 'data_command' && receivedData.includes('354')) {
      currentStep = 'message_body';
      receivedData = '';
      // Send email content
      socket.write('From: sender@example.com\r\n');
      socket.write('To: recipient@example.com\r\n');
      socket.write('Subject: Test message\r\n');
      socket.write('\r\n'); // Empty line to separate headers from body
      socket.write('This is a test message.\r\n');
      socket.write('.\r\n'); // End of message
    } else if (currentStep === 'message_body' && receivedData.includes('250')) {
      expect(receivedData).toInclude('250');
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        done.resolve();
      }, 100);
    }
  });
  
  socket.on('error', (error) => {
    done.reject(error);
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

tap.test('DATA - should reject without RCPT TO', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  
  socket.on('data', (data) => {
    receivedData += data.toString();
    
    if (currentStep === 'connecting' && receivedData.includes('220')) {
      currentStep = 'ehlo';
      receivedData = '';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
      currentStep = 'data_without_rcpt';
      receivedData = '';
      // Try DATA without MAIL FROM or RCPT TO
      socket.write('DATA\r\n');
    } else if (currentStep === 'data_without_rcpt' && receivedData.includes('503')) {
      // Should get 503 (bad sequence)
      expect(receivedData).toInclude('503');
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        done.resolve();
      }, 100);
    }
  });
  
  socket.on('error', (error) => {
    done.reject(error);
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

tap.test('DATA - should accept empty message body', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  
  socket.on('data', (data) => {
    receivedData += data.toString();
    
    if (currentStep === 'connecting' && receivedData.includes('220')) {
      currentStep = 'ehlo';
      receivedData = '';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
      currentStep = 'mail_from';
      receivedData = '';
      socket.write('MAIL FROM:<sender@example.com>\r\n');
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
      currentStep = 'rcpt_to';
      receivedData = '';
      socket.write('RCPT TO:<recipient@example.com>\r\n');
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
      currentStep = 'data_command';
      receivedData = '';
      socket.write('DATA\r\n');
    } else if (currentStep === 'data_command' && receivedData.includes('354')) {
      currentStep = 'empty_message';
      receivedData = '';
      // Send only the terminator
      socket.write('.\r\n');
    } else if (currentStep === 'empty_message') {
      // Server should accept empty message
      expect(receivedData).toMatch(/^(250|5\d\d)/);
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        done.resolve();
      }, 100);
    }
  });
  
  socket.on('error', (error) => {
    done.reject(error);
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

tap.test('DATA - should handle dot stuffing correctly', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  
  socket.on('data', (data) => {
    receivedData += data.toString();
    
    if (currentStep === 'connecting' && receivedData.includes('220')) {
      currentStep = 'ehlo';
      receivedData = '';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
      currentStep = 'mail_from';
      receivedData = '';
      socket.write('MAIL FROM:<sender@example.com>\r\n');
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
      currentStep = 'rcpt_to';
      receivedData = '';
      socket.write('RCPT TO:<recipient@example.com>\r\n');
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
      currentStep = 'data_command';
      receivedData = '';
      socket.write('DATA\r\n');
    } else if (currentStep === 'data_command' && receivedData.includes('354')) {
      currentStep = 'dot_stuffed_message';
      receivedData = '';
      // Send message with dots that need stuffing
      socket.write('This line is normal.\r\n');
      socket.write('..This line starts with two dots (one will be removed).\r\n');
      socket.write('.This line starts with a single dot.\r\n');
      socket.write('...This line starts with three dots.\r\n');
      socket.write('.\r\n'); // End of message
    } else if (currentStep === 'dot_stuffed_message' && receivedData.includes('250')) {
      expect(receivedData).toInclude('250');
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        done.resolve();
      }, 100);
    }
  });
  
  socket.on('error', (error) => {
    done.reject(error);
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

tap.test('DATA - should handle large messages', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  
  socket.on('data', (data) => {
    receivedData += data.toString();
    
    if (currentStep === 'connecting' && receivedData.includes('220')) {
      currentStep = 'ehlo';
      receivedData = '';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
      currentStep = 'mail_from';
      receivedData = '';
      socket.write('MAIL FROM:<sender@example.com>\r\n');
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
      currentStep = 'rcpt_to';
      receivedData = '';
      socket.write('RCPT TO:<recipient@example.com>\r\n');
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
      currentStep = 'data_command';
      receivedData = '';
      socket.write('DATA\r\n');
    } else if (currentStep === 'data_command' && receivedData.includes('354')) {
      currentStep = 'large_message';
      receivedData = '';
      // Send a large message (100KB)
      socket.write('From: sender@example.com\r\n');
      socket.write('To: recipient@example.com\r\n');
      socket.write('Subject: Large test message\r\n');
      socket.write('\r\n');
      
      // Generate 100KB of data
      const lineContent = 'This is a test line that will be repeated many times. ';
      const linesNeeded = Math.ceil(100000 / lineContent.length);
      
      for (let i = 0; i < linesNeeded; i++) {
        socket.write(lineContent + '\r\n');
      }
      
      socket.write('.\r\n'); // End of message
    } else if (currentStep === 'large_message' && receivedData.includes('250')) {
      expect(receivedData).toInclude('250');
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        done.resolve();
      }, 100);
    }
  });
  
  socket.on('error', (error) => {
    done.reject(error);
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

tap.test('DATA - should handle binary data in message', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  
  socket.on('data', (data) => {
    receivedData += data.toString();
    
    if (currentStep === 'connecting' && receivedData.includes('220')) {
      currentStep = 'ehlo';
      receivedData = '';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
      currentStep = 'mail_from';
      receivedData = '';
      socket.write('MAIL FROM:<sender@example.com>\r\n');
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
      currentStep = 'rcpt_to';
      receivedData = '';
      socket.write('RCPT TO:<recipient@example.com>\r\n');
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
      currentStep = 'data_command';
      receivedData = '';
      socket.write('DATA\r\n');
    } else if (currentStep === 'data_command' && receivedData.includes('354')) {
      currentStep = 'binary_message';
      receivedData = '';
      // Send message with binary data (base64 encoded attachment)
      socket.write('From: sender@example.com\r\n');
      socket.write('To: recipient@example.com\r\n');
      socket.write('Subject: Binary test message\r\n');
      socket.write('MIME-Version: 1.0\r\n');
      socket.write('Content-Type: multipart/mixed; boundary="boundary123"\r\n');
      socket.write('\r\n');
      socket.write('--boundary123\r\n');
      socket.write('Content-Type: text/plain\r\n');
      socket.write('\r\n');
      socket.write('This message contains binary data.\r\n');
      socket.write('--boundary123\r\n');
      socket.write('Content-Type: application/octet-stream\r\n');
      socket.write('Content-Transfer-Encoding: base64\r\n');
      socket.write('\r\n');
      socket.write('SGVsbG8gV29ybGQhIFRoaXMgaXMgYmluYXJ5IGRhdGEu\r\n');
      socket.write('--boundary123--\r\n');
      socket.write('.\r\n'); // End of message
    } else if (currentStep === 'binary_message' && receivedData.includes('250')) {
      expect(receivedData).toInclude('250');
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        done.resolve();
      }, 100);
    }
  });
  
  socket.on('error', (error) => {
    done.reject(error);
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

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

export default tap.start();