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';

// Test configuration
const TEST_PORT = 2525;

let testServer;
const TEST_TIMEOUT = 10000;

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

// Test: Basic QUIT command
tap.test('QUIT - should close connection gracefully', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  let connectionClosed = false;
  
  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';
      socket.write('QUIT\r\n');
    } else if (currentStep === 'quit' && receivedData.includes('221')) {
      // Don't destroy immediately, wait for server to close connection
      setTimeout(() => {
        if (!connectionClosed) {
          socket.destroy();
          expect(receivedData).toInclude('221'); // Closing connection message
          done.resolve();
        }
      }, 2000);
    }
  });
  
  socket.on('close', () => {
    if (currentStep === 'quit' && receivedData.includes('221')) {
      connectionClosed = true;
      expect(receivedData).toInclude('221');
      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: QUIT during transaction
tap.test('QUIT - should work during active transaction', 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';
      socket.write('RCPT TO:<recipient@example.com>\r\n');
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
      currentStep = 'quit';
      socket.write('QUIT\r\n');
    } else if (currentStep === 'quit' && receivedData.includes('221')) {
      setTimeout(() => {
        socket.destroy();
        expect(receivedData).toInclude('221');
        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: QUIT immediately after connect
tap.test('QUIT - should work immediately after connection', 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 = 'quit';
      socket.write('QUIT\r\n');
    } else if (currentStep === 'quit' && receivedData.includes('221')) {
      setTimeout(() => {
        socket.destroy();
        expect(receivedData).toInclude('221');
        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: QUIT with parameters (should be ignored or rejected)
tap.test('QUIT - should handle QUIT with 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';
      receivedData = '';
      socket.write('EHLO test.example.com\r\n');
    } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
      currentStep = 'quit_with_param';
      receivedData = '';
      socket.write('QUIT unexpected parameter\r\n');
    } else if (currentStep === 'quit_with_param' && (receivedData.includes('221') || receivedData.includes('501'))) {
      // Server may accept (221) or reject (501) QUIT with parameters
      const responseCode = receivedData.match(/(\d{3})/)?.[1];
      socket.destroy();
      expect(['221', '501']).toInclude(responseCode);
      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: Multiple QUITs (second should fail)
tap.test('QUIT - second QUIT should fail after connection closed', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  let quitSent = false;
  
  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 = 'quit';
      receivedData = '';
      socket.write('QUIT\r\n');
      quitSent = true;
    } else if (currentStep === 'quit' && receivedData.includes('221')) {
      // Try to send another QUIT
      try {
        socket.write('QUIT\r\n');
        // If write succeeds, wait a bit to see if we get a response
        setTimeout(() => {
          socket.destroy();
          done.resolve(); // Test passes either way
        }, 500);
      } catch (err) {
        // Write failed because connection closed - this is expected
        done.resolve();
      }
    }
  });
  
  socket.on('close', () => {
    if (quitSent) {
      done.resolve();
    }
  });
  
  socket.on('error', (error) => {
    if (quitSent && error.message.includes('EPIPE')) {
      // Expected error when writing to closed socket
      done.resolve();
    } else {
      done.reject(error);
    }
  });
  
  socket.on('timeout', () => {
    socket.destroy();
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
  });
  
  await done.promise;
});

// Test: QUIT response format
tap.test('QUIT - should return proper 221 response', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  let quitResponse = '';
  
  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';
      receivedData = ''; // Clear buffer to capture only QUIT response
      socket.write('QUIT\r\n');
    } else if (currentStep === 'quit' && receivedData.includes('221')) {
      quitResponse = receivedData.trim();
      setTimeout(() => {
        socket.destroy();
        expect(quitResponse).toStartWith('221');
        expect(quitResponse.toLowerCase()).toInclude('closing');
        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: Connection cleanup after QUIT
tap.test('QUIT - verify clean connection shutdown', async (tools) => {
  const done = tools.defer();
  
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: TEST_TIMEOUT
  });
  
  let receivedData = '';
  let currentStep = 'connecting';
  let closeEventFired = false;
  let endEventFired = false;
  
  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';
      socket.write('QUIT\r\n');
    } else if (currentStep === 'quit' && receivedData.includes('221')) {
      // Wait for clean shutdown
      setTimeout(() => {
        if (!closeEventFired && !endEventFired) {
          socket.destroy();
          done.resolve();
        }
      }, 3000);
    }
  });
  
  socket.on('end', () => {
    endEventFired = true;
  });
  
  socket.on('close', () => {
    closeEventFired = true;
    if (currentStep === 'quit') {
      expect(endEventFired || closeEventFired).toEqual(true);
      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;
});

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

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