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

// Test configuration
const TEST_PORT = 2525;
const TEST_TIMEOUT = 10000;

let testServer: ITestServer;

// Setup
tap.test('setup - start SMTP server', async () => {
  testServer = await startTestServer({
    port: TEST_PORT,
    tlsEnabled: false,
    hostname: 'localhost'
  });
  
  expect(testServer).toBeDefined();
  expect(testServer.port).toEqual(TEST_PORT);
});

// Test: Invalid command
tap.test('Syntax Errors - should reject invalid command', 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 = 'invalid_command';
      socket.write('INVALID_COMMAND\r\n');
    } else if (currentStep === 'invalid_command' && receivedData.match(/[45]\d{2}/)) {
      // Extract response code immediately after receiving error response
      const lines = receivedData.split('\r\n');
      // Find the last line that starts with 4xx or 5xx
      let errorCode = '';
      for (let i = lines.length - 1; i >= 0; i--) {
        const match = lines[i].match(/^([45]\d{2})\s/);
        if (match) {
          errorCode = match[1];
          break;
        }
      }
      
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        // Expect 500 (syntax error) or 502 (command not implemented)
        expect(errorCode).toMatch(/^(500|502)$/);
        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;
});

// Test: MAIL FROM without brackets
tap.test('Syntax Errors - should reject MAIL FROM without brackets', 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';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250')) {
      currentStep = 'mail_from_no_brackets';
      socket.write('MAIL FROM:test@example.com\r\n'); // Missing angle brackets
    } else if (currentStep === 'mail_from_no_brackets' && receivedData.match(/[45]\d{2}/)) {
      // Extract the most recent error response code
      const lines = receivedData.split('\r\n');
      let responseCode = '';
      for (let i = lines.length - 1; i >= 0; i--) {
        const match = lines[i].match(/^([45]\d{2})\s/);
        if (match) {
          responseCode = match[1];
          break;
        }
      }
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        // Expect 501 (syntax error in parameters)
        expect(responseCode).toEqual('501');
        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;
});

// Test: RCPT TO without brackets
tap.test('Syntax Errors - should reject RCPT TO without brackets', 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';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250')) {
      currentStep = 'mail_from';
      socket.write('MAIL FROM:<sender@example.com>\r\n');
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
      currentStep = 'rcpt_to_no_brackets';
      socket.write('RCPT TO:recipient@example.com\r\n'); // Missing angle brackets
    } else if (currentStep === 'rcpt_to_no_brackets' && receivedData.match(/[45]\d{2}/)) {
      // Extract the most recent error response code
      const lines = receivedData.split('\r\n');
      let responseCode = '';
      for (let i = lines.length - 1; i >= 0; i--) {
        const match = lines[i].match(/^([45]\d{2})\s/);
        if (match) {
          responseCode = match[1];
          break;
        }
      }
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        // Expect 501 (syntax error in parameters)
        expect(responseCode).toEqual('501');
        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;
});

// Test: EHLO without hostname
tap.test('Syntax Errors - should reject EHLO without hostname', 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_no_hostname';
      socket.write('EHLO\r\n'); // Missing hostname
    } else if (currentStep === 'ehlo_no_hostname' && receivedData.match(/[45]\d{2}/)) {
      // Extract the most recent error response code
      const lines = receivedData.split('\r\n');
      let responseCode = '';
      for (let i = lines.length - 1; i >= 0; i--) {
        const match = lines[i].match(/^([45]\d{2})\s/);
        if (match) {
          responseCode = match[1];
          break;
        }
      }
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        // Expect 501 (syntax error in parameters)
        expect(responseCode).toEqual('501');
        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;
});

// Test: Command with extra parameters
tap.test('Syntax Errors - should handle commands with extra parameters', 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';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250')) {
      currentStep = 'quit_extra';
      socket.write('QUIT extra parameters\r\n'); // QUIT doesn't take parameters
    } else if (currentStep === 'quit_extra') {
      // Extract the most recent response code (could be 221 or error)
      const lines = receivedData.split('\r\n');
      let responseCode = '';
      for (let i = lines.length - 1; i >= 0; i--) {
        const match = lines[i].match(/^([2-5]\d{2})\s/);
        if (match) {
          responseCode = match[1];
          break;
        }
      }
      socket.destroy();
      // Some servers might accept it (221) or reject it (501)
      expect(responseCode).toMatch(/^(221|501)$/);
      done.resolve();
    }
  });
  
  socket.on('error', (error) => {
    done.reject(error);
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

// Test: Malformed addresses
tap.test('Syntax Errors - should reject malformed email addresses', 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';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250')) {
      currentStep = 'mail_from_malformed';
      socket.write('MAIL FROM:<not an email>\r\n'); // Malformed address
    } else if (currentStep === 'mail_from_malformed' && receivedData.match(/[45]\d{2}/)) {
      // Extract the most recent error response code
      const lines = receivedData.split('\r\n');
      let responseCode = '';
      for (let i = lines.length - 1; i >= 0; i--) {
        const match = lines[i].match(/^([45]\d{2})\s/);
        if (match) {
          responseCode = match[1];
          break;
        }
      }
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        // Expect 501 or 553 (bad address)
        expect(responseCode).toMatch(/^(501|553)$/);
        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;
});

// Test: Commands in wrong order
tap.test('Syntax Errors - should reject commands in wrong sequence', 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 = 'data_without_rcpt';
      socket.write('DATA\r\n'); // DATA without MAIL FROM/RCPT TO
    } else if (currentStep === 'data_without_rcpt' && receivedData.match(/[45]\d{2}/)) {
      // Extract the most recent error response code
      const lines = receivedData.split('\r\n');
      let responseCode = '';
      for (let i = lines.length - 1; i >= 0; i--) {
        const match = lines[i].match(/^([45]\d{2})\s/);
        if (match) {
          responseCode = match[1];
          break;
        }
      }
      socket.write('QUIT\r\n');
      setTimeout(() => {
        socket.destroy();
        // Expect 503 (bad sequence of commands)
        expect(responseCode).toEqual('503');
        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;
});

// Test: Long commands
tap.test('Syntax Errors - should handle excessively long commands', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  const longString = 'A'.repeat(1000); // Very long string
  
  socket.on('data', (data) => {
    receivedData += data.toString();
    
    if (currentStep === 'connecting' && receivedData.includes('220')) {
      currentStep = 'long_command';
      socket.write(`EHLO ${longString}\r\n`); // Excessively long hostname
    } else if (currentStep === 'long_command') {
      // Wait for complete response (including all continuation lines)
      if (receivedData.includes('250 ') || receivedData.match(/[45]\d{2}\s/)) {
        currentStep = 'done';
        
        // The server accepted the long EHLO command with 250
        // Some servers might reject with 500/501
        // Since we see 250 in the logs, the server accepts it
        const hasError = receivedData.match(/([45]\d{2})\s/);
        const hasSuccess = receivedData.includes('250 ');
        
        // Determine the response code
        let responseCode = '';
        if (hasError) {
          responseCode = hasError[1];
        } else if (hasSuccess) {
          responseCode = '250';
        }
        
        // Some servers accept long hostnames, others reject them
        // Accept either 250 (ok), 500 (syntax error), or 501 (line too long)
        expect(responseCode).toMatch(/^(250|500|501)$/);
        
        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;
});

// Teardown
tap.test('teardown - stop SMTP server', async () => {
  if (testServer) {
    await stopTestServer(testServer);
  }
});

// Start the test
export default tap.start();