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

const TEST_PORT = 30035;
const TEST_TIMEOUT = 30000;

let testServer: ITestServer;

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

tap.test('Invalid Character Handling - should handle control characters in email', 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 envelope
    socket.write('MAIL FROM:<sender@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('RCPT TO:<recipient@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('DATA\r\n');
    const dataResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(dataResponse).toInclude('354');
    
    // Test with control characters
    const controlChars = [
      '\x00', // NULL
      '\x01', // SOH
      '\x02', // STX
      '\x03', // ETX
      '\x7F'  // DEL
    ];
    
    const emailWithControlChars = 
      'From: sender@example.com\r\n' +
      'To: recipient@example.com\r\n' +
      `Subject: Control Character Test ${controlChars.join('')}\r\n` +
      '\r\n' +
      `This email contains control characters: ${controlChars.join('')}\r\n` +
      'Null byte: \x00\r\n' +
      'Delete char: \x7F\r\n' +
      '.\r\n';
    
    socket.write(emailWithControlChars);
    
    const finalResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to control characters:', finalResponse);
    
    // Server might accept or reject based on security settings
    const accepted = finalResponse.includes('250');
    const rejected = finalResponse.includes('550') || finalResponse.includes('554');
    
    expect(accepted || rejected).toEqual(true);
    
    if (rejected) {
      console.log('Server rejected control characters (strict security)');
    } else {
      console.log('Server accepted control characters (may sanitize internally)');
    }
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Invalid Character Handling - should handle high-byte characters', 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 envelope
    socket.write('MAIL FROM:<sender@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('RCPT TO:<recipient@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('DATA\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Test with high-byte characters
    const highByteChars = [
      '\xFF', // 255
      '\xFE', // 254
      '\xFD', // 253
      '\xFC', // 252
      '\xFB'  // 251
    ];
    
    const emailWithHighBytes = 
      'From: sender@example.com\r\n' +
      'To: recipient@example.com\r\n' +
      'Subject: High-byte Character Test\r\n' +
      '\r\n' +
      `High-byte characters: ${highByteChars.join('')}\r\n` +
      'Extended ASCII: \xE0\xE1\xE2\xE3\xE4\r\n' +
      '.\r\n';
    
    socket.write(emailWithHighBytes);
    
    const finalResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to high-byte characters:', finalResponse);
    
    // Both acceptance and rejection are valid
    expect(finalResponse).toMatch(/^[2-5]\d{2}/);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Invalid Character Handling - should handle Unicode special characters', 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 envelope
    socket.write('MAIL FROM:<sender@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('RCPT TO:<recipient@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('DATA\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Test with Unicode special characters
    const unicodeSpecials = [
      '\u2000', // EN QUAD
      '\u2028', // LINE SEPARATOR
      '\u2029', // PARAGRAPH SEPARATOR
      '\uFEFF', // ZERO WIDTH NO-BREAK SPACE (BOM)
      '\u200B', // ZERO WIDTH SPACE
      '\u200C', // ZERO WIDTH NON-JOINER
      '\u200D'  // ZERO WIDTH JOINER
    ];
    
    const emailWithUnicode = 
      'From: sender@example.com\r\n' +
      'To: recipient@example.com\r\n' +
      'Subject: Unicode Special Characters Test\r\n' +
      'Content-Type: text/plain; charset=utf-8\r\n' +
      '\r\n' +
      `Unicode specials: ${unicodeSpecials.join('')}\r\n` +
      'Line separator: \u2028\r\n' +
      'Paragraph separator: \u2029\r\n' +
      'Zero-width space: word\u200Bword\r\n' +
      '.\r\n';
    
    socket.write(emailWithUnicode);
    
    const finalResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to Unicode special characters:', finalResponse);
    
    // Most servers should accept Unicode with proper charset declaration
    expect(finalResponse).toMatch(/^[2-5]\d{2}/);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Invalid Character Handling - should handle bare LF and CR', 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 envelope
    socket.write('MAIL FROM:<sender@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('RCPT TO:<recipient@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('DATA\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Test with bare LF and CR (not allowed in SMTP)
    const emailWithBareLfCr = 
      'From: sender@example.com\r\n' +
      'To: recipient@example.com\r\n' +
      'Subject: Bare LF and CR Test\r\n' +
      '\r\n' +
      'Line with bare LF:\nThis should not be allowed\r\n' +
      'Line with bare CR:\rThis should also not be allowed\r\n' +
      'Correct line ending\r\n' +
      '.\r\n';
    
    socket.write(emailWithBareLfCr);
    
    const finalResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to bare LF/CR:', finalResponse);
    
    // Servers may accept and fix, or reject
    const accepted = finalResponse.includes('250');
    const rejected = finalResponse.includes('550') || finalResponse.includes('554');
    
    if (accepted) {
      console.log('Server accepted bare LF/CR (may convert to CRLF)');
    } else if (rejected) {
      console.log('Server rejected bare LF/CR (strict SMTP compliance)');
    }
    
    expect(accepted || rejected).toEqual(true);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    done.resolve();
  }
});

tap.test('Invalid Character Handling - should handle long lines without proper folding', 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 envelope
    socket.write('MAIL FROM:<sender@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('RCPT TO:<recipient@example.com>\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    socket.write('DATA\r\n');
    await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    // Create a line that exceeds RFC 5322 limit (998 characters)
    const longLine = 'X'.repeat(1500);
    
    const emailWithLongLine = 
      'From: sender@example.com\r\n' +
      'To: recipient@example.com\r\n' +
      'Subject: Long Line Test\r\n' +
      '\r\n' +
      'Normal line\r\n' +
      longLine + '\r\n' +
      'Another normal line\r\n' +
      '.\r\n';
    
    socket.write(emailWithLongLine);
    
    const finalResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    console.log('Response to long line:', finalResponse);
    console.log(`Line length: ${longLine.length} characters`);
    
    // Server should handle this (accept, wrap, or reject)
    expect(finalResponse).toMatch(/^[2-5]\d{2}/);
    
    // 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();