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

const TEST_PORT = 30036;
const TEST_TIMEOUT = 30000;

let testServer: ITestServer;

tap.test('setup - start SMTP server for empty command tests', async () => {
  testServer = await startTestServer({
    port: TEST_PORT,
    hostname: 'localhost'
  });
  expect(testServer).toBeDefined();
});

tap.test('Empty Commands - should reject empty line (just CRLF)', 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 first
    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 empty line (just CRLF)
    socket.write('\r\n');
    
    const response = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
      setTimeout(() => resolve('TIMEOUT'), 2000);
    });
    
    console.log('Response to empty line:', response);
    
    // Should get syntax error (500, 501, or 502)
    if (response !== 'TIMEOUT') {
      expect(response).toMatch(/^5\d{2}/);
    } else {
      // Server might ignore empty lines
      console.log('Server ignored empty line');
      expect(true).toEqual(true);
    }
    
    // Test server is still responsive
    socket.write('NOOP\r\n');
    const noopResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(noopResponse).toInclude('250');
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Empty Commands - should reject commands with only whitespace', 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 and send EHLO
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    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);
    });
    
    // Test various whitespace-only commands
    const whitespaceCommands = [
      '   \r\n',    // Spaces only
      '\t\r\n',     // Tab only
      ' \t \r\n',   // Mixed whitespace
      '     \r\n'   // Multiple spaces
    ];
    
    for (const cmd of whitespaceCommands) {
      socket.write(cmd);
      
      const response = await new Promise<string>((resolve) => {
        socket.once('data', (chunk) => resolve(chunk.toString()));
        setTimeout(() => resolve('TIMEOUT'), 2000);
      });
      
      console.log(`Response to whitespace "${cmd.trim()}"\\r\\n:`, response);
      
      if (response !== 'TIMEOUT') {
        // Should get syntax error
        expect(response).toMatch(/^5\d{2}/);
      }
    }
    
    // Verify server still works
    socket.write('NOOP\r\n');
    const noopResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(noopResponse).toInclude('250');
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Empty Commands - should reject MAIL FROM with empty parameter', 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);
    });
    
    // Setup connection
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    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 MAIL FROM with empty parameter
    socket.write('MAIL FROM:\r\n');
    
    const response = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to empty MAIL FROM:', response);
    
    // Should get syntax error (501 or 550)
    expect(response).toMatch(/^5\d{2}/);
    expect(response).toMatch(/syntax|parameter|address/i);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Empty Commands - should reject RCPT TO with empty parameter', 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);
    });
    
    // Setup connection
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    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 valid MAIL FROM first
    socket.write('MAIL FROM:<sender@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Send RCPT TO with empty parameter
    socket.write('RCPT TO:\r\n');
    
    const response = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to empty RCPT TO:', response);
    
    // Should get syntax error
    expect(response).toMatch(/^5\d{2}/);
    expect(response).toMatch(/syntax|parameter|address/i);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Empty Commands - should reject EHLO/HELO without hostname', 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 without hostname
    socket.write('EHLO\r\n');
    
    const ehloResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to EHLO without hostname:', ehloResponse);
    
    // Should get syntax error
    expect(ehloResponse).toMatch(/^5\d{2}/);
    
    // Try HELO without hostname
    socket.write('HELO\r\n');
    
    const heloResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to HELO without hostname:', heloResponse);
    
    // Should get syntax error
    expect(heloResponse).toMatch(/^5\d{2}/);
    
    // Send valid EHLO to establish session
    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);
    });
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Empty Commands - server should remain stable after empty 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 empty/invalid commands
    const invalidCommands = [
      '\r\n',
      '   \r\n',
      'MAIL FROM:\r\n',
      'RCPT TO:\r\n',
      'EHLO\r\n',
      '\t\r\n'
    ];
    
    for (const cmd of invalidCommands) {
      socket.write(cmd);
      
      // Read response but don't fail if error
      await new Promise<string>((resolve) => {
        socket.once('data', (chunk) => resolve(chunk.toString()));
        setTimeout(() => resolve('TIMEOUT'), 1000);
      });
    }
    
    // Now test that server is still functional
    socket.write('MAIL FROM:<test@example.com>\r\n');
    const mailResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(mailResponse).toInclude('250');
    
    socket.write('RCPT TO:<recipient@example.com>\r\n');
    const rcptResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(rcptResponse).toInclude('250');
    
    console.log('Server remained stable after multiple empty commands');
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('cleanup - stop SMTP server', async () => {
  await stopTestServer(testServer);
  expect(true).toEqual(true);
});

export default tap.start();