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

let testServer: ITestServer;
const TEST_TIMEOUT = 60000; // Longer timeout for keepalive tests

tap.test('Keepalive - should maintain TCP keepalive', async (tools) => {
  const done = tools.defer();
  
  // Start test server
  testServer = await startTestServer({ port: TEST_PORT });
  
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  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);
    });
    
    // Enable TCP keepalive
    const keepAliveDelay = 1000; // 1 second
    socket.setKeepAlive(true, keepAliveDelay);
    console.log(`TCP keepalive enabled with ${keepAliveDelay}ms delay`);
    
    // Get banner
    const banner = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(banner).toInclude('220');
    
    // Send EHLO
    socket.write('EHLO testhost\r\n');
    
    const ehloResponse = 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);
    });
    
    expect(ehloResponse).toInclude('250');
    
    // Wait for keepalive duration + buffer
    console.log('Waiting for keepalive period...');
    await new Promise(resolve => setTimeout(resolve, keepAliveDelay + 500));
    
    // Verify connection is still alive by sending NOOP
    socket.write('NOOP\r\n');
    
    const noopResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(noopResponse).toInclude('250');
    console.log('Connection maintained after keepalive period');
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    await stopTestServer(testServer);
    done.resolve();
  }
});

tap.test('Keepalive - should maintain idle connection for extended period', async (tools) => {
  const done = tools.defer();
  
  // Start test server
  testServer = await startTestServer({ port: TEST_PORT });
  
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  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);
    });
    
    // Enable keepalive
    socket.setKeepAlive(true, 1000);
    
    // 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);
    });
    
    // Test multiple keepalive periods
    const periods = 3;
    const periodDuration = 1000; // 1 second each
    
    for (let i = 0; i < periods; i++) {
      console.log(`Keepalive period ${i + 1}/${periods}...`);
      await new Promise(resolve => setTimeout(resolve, periodDuration));
      
      // Send NOOP to verify connection
      socket.write('NOOP\r\n');
      
      const response = await new Promise<string>((resolve) => {
        socket.once('data', (chunk) => resolve(chunk.toString()));
      });
      
      expect(response).toInclude('250');
      console.log(`Connection alive after ${(i + 1) * periodDuration}ms`);
    }
    
    console.log(`Connection maintained for ${periods * periodDuration}ms total`);
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    await stopTestServer(testServer);
    done.resolve();
  }
});

tap.test('Keepalive - should detect connection loss', async (tools) => {
  const done = tools.defer();
  
  // Start test server
  testServer = await startTestServer({ port: TEST_PORT });
  
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  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);
    });
    
    // Enable keepalive with short interval
    socket.setKeepAlive(true, 1000);
    
    // Track connection state
    let connectionLost = false;
    socket.on('close', () => {
      connectionLost = true;
      console.log('Connection closed');
    });
    
    socket.on('error', (err) => {
      connectionLost = true;
      console.log('Connection error:', err.message);
    });
    
    // 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);
    });
    
    console.log('Connection established, now simulating server shutdown...');
    
    // Shutdown server to simulate connection loss
    await stopTestServer(testServer);
    
    // Wait for keepalive to detect connection loss
    await new Promise(resolve => setTimeout(resolve, 3000));
    
    // Connection should be detected as lost
    expect(connectionLost).toEqual(true);
    console.log('Keepalive detected connection loss');
    
  } finally {
    // Server already shutdown, just resolve
    done.resolve();
  }
});

tap.test('Keepalive - should handle long-running SMTP session', async (tools) => {
  const done = tools.defer();
  
  // Start test server
  testServer = await startTestServer({ port: TEST_PORT });
  
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  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);
    });
    
    // Enable keepalive
    socket.setKeepAlive(true, 2000);
    
    const sessionStart = Date.now();
    
    // 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);
    });
    
    // Simulate a long-running session with periodic activity
    const activities = [
      { command: 'MAIL FROM:<sender1@example.com>', delay: 500 },
      { command: 'RSET', delay: 500 },
      { command: 'MAIL FROM:<sender2@example.com>', delay: 500 },
      { command: 'RSET', delay: 500 }
    ];
    
    for (const activity of activities) {
      await new Promise(resolve => setTimeout(resolve, activity.delay));
      
      console.log(`Sending: ${activity.command}`);
      socket.write(`${activity.command}\r\n`);
      
      const response = await new Promise<string>((resolve) => {
        socket.once('data', (chunk) => resolve(chunk.toString()));
      });
      
      expect(response).toInclude('250');
    }
    
    const sessionDuration = Date.now() - sessionStart;
    console.log(`Long-running session maintained for ${sessionDuration}ms`);
    
    // Clean up
    socket.write('QUIT\r\n');
    
    const quitResponse = await new Promise<string>((resolve) => {
      socket.once('data', (chunk) => resolve(chunk.toString()));
    });
    
    expect(quitResponse).toInclude('221');
    socket.end();
    
  } finally {
    await stopTestServer(testServer);
    done.resolve();
  }
});

tap.test('Keepalive - should handle NOOP as keepalive mechanism', async (tools) => {
  const done = tools.defer();
  
  // Start test server
  testServer = await startTestServer({ port: TEST_PORT });
  
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  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);
    });
    
    // Use NOOP as application-level keepalive
    const noopInterval = 1000; // 1 second
    const noopCount = 3;
    
    console.log(`Sending ${noopCount} NOOP commands as keepalive...`);
    
    for (let i = 0; i < noopCount; i++) {
      await new Promise(resolve => setTimeout(resolve, noopInterval));
      
      socket.write('NOOP\r\n');
      
      const response = await new Promise<string>((resolve) => {
        socket.once('data', (chunk) => resolve(chunk.toString()));
      });
      
      expect(response).toInclude('250');
      console.log(`NOOP ${i + 1}/${noopCount} successful`);
    }
    
    console.log('Application-level keepalive successful');
    
    // Clean up
    socket.write('QUIT\r\n');
    socket.end();
    
  } finally {
    await stopTestServer(testServer);
    done.resolve();
  }
});

export default tap.start();