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

const TEST_PORT = 2525;

let testServer;
const activeSockets = new Set<net.Socket>();

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

tap.test('ERR-07: Exception handling - Invalid commands', async (tools) => {
  const done = tools.defer();
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: 30000
  });
  
  activeSockets.add(socket);
  socket.on('close', () => activeSockets.delete(socket));

  socket.on('connect', async () => {
    try {
      // Read greeting
      await new Promise<void>((resolve) => {
        socket.once('data', () => resolve());
      });

      // Send EHLO
      socket.write('EHLO testhost\r\n');
      
      await new Promise<void>((resolve) => {
        let data = '';
        const handleData = (chunk: Buffer) => {
          data += chunk.toString();
          if (data.includes('250 ') && data.includes('\r\n')) {
            socket.removeListener('data', handleData);
            resolve();
          }
        };
        socket.on('data', handleData);
      });

      // Test various exception-triggering commands
      const invalidCommands = [
        'INVALID_COMMAND_THAT_SHOULD_TRIGGER_EXCEPTION',
        'MAIL FROM:<>',  // Empty address
        'RCPT TO:<>',    // Empty address
        '\x00\x01\x02INVALID_BYTES',  // Binary data
        'VERY_LONG_COMMAND_' + 'X'.repeat(1000),  // Excessively long command
        'MAIL FROM',     // Missing parameter
        'RCPT TO',       // Missing parameter
        'DATA DATA DATA' // Invalid syntax
      ];

      let exceptionHandled = false;
      let serverStillResponding = true;

      for (const command of invalidCommands) {
        try {
          socket.write(command + '\r\n');
          
          const response = await new Promise<string>((resolve, reject) => {
            const timeout = setTimeout(() => {
              reject(new Error('Timeout waiting for response'));
            }, 5000);

            socket.once('data', (chunk) => {
              clearTimeout(timeout);
              resolve(chunk.toString());
            });
          });

          console.log(`Command: "${command.substring(0, 50)}..." -> Response: ${response.substring(0, 50)}`);

          // Check if server handled the exception properly
          if (response.includes('500') ||  // Command not recognized
              response.includes('501') ||  // Syntax error
              response.includes('502') ||  // Command not implemented
              response.includes('503') ||  // Bad sequence
              response.includes('error') ||
              response.includes('invalid')) {
            exceptionHandled = true;
          }

          // Small delay between commands
          await new Promise(resolve => setTimeout(resolve, 100));

        } catch (err) {
          console.log('Error with command:', command, err);
          // Connection might be closed by server - that's ok for some commands
          serverStillResponding = false;
          break;
        }
      }

      // If still connected, verify server is still responsive
      if (serverStillResponding) {
        try {
          socket.write('NOOP\r\n');
          const noopResponse = await new Promise<string>((resolve, reject) => {
            const timeout = setTimeout(() => {
              reject(new Error('Timeout on NOOP'));
            }, 5000);

            socket.once('data', (chunk) => {
              clearTimeout(timeout);
              resolve(chunk.toString());
            });
          });

          if (noopResponse.includes('250')) {
            serverStillResponding = true;
          }
        } catch (err) {
          serverStillResponding = false;
        }
      }

      console.log('Exception handled:', exceptionHandled);
      console.log('Server still responding:', serverStillResponding);

      // Test passes if exceptions were handled OR server is still responding
      expect(exceptionHandled || serverStillResponding).toEqual(true);

      if (socket.writable) {
        socket.write('QUIT\r\n');
      }
      socket.end();
      done.resolve();
    } catch (error) {
      socket.end();
      done.reject(error);
    }
  });

  socket.on('error', (error) => {
    done.reject(error);
  });
});

tap.test('ERR-07: Exception handling - Malformed protocol', async (tools) => {
  const done = tools.defer();
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: 30000
  });
  
  activeSockets.add(socket);
  socket.on('close', () => activeSockets.delete(socket));

  socket.on('connect', async () => {
    try {
      // Read greeting
      await new Promise<void>((resolve) => {
        socket.once('data', () => resolve());
      });

      // Send commands with protocol violations
      const protocolViolations = [
        'EHLO',  // No hostname
        'MAIL FROM:<test@example.com> SIZE=',  // Incomplete SIZE
        'RCPT TO:<test@example.com> NOTIFY=',  // Incomplete NOTIFY
        'AUTH PLAIN',  // No credentials
        'STARTTLS EXTRA',  // Extra parameters
        'MAIL FROM:<test@example.com>\r\nRCPT TO:<test@example.com>',  // Multiple commands in one line
      ];

      let violationsHandled = 0;

      for (const violation of protocolViolations) {
        try {
          socket.write(violation + '\r\n');
          
          const response = await new Promise<string>((resolve) => {
            const timeout = setTimeout(() => {
              resolve('TIMEOUT');
            }, 3000);

            socket.once('data', (chunk) => {
              clearTimeout(timeout);
              resolve(chunk.toString());
            });
          });

          if (response !== 'TIMEOUT' && 
              (response.includes('500') || 
               response.includes('501') || 
               response.includes('503'))) {
            violationsHandled++;
          }

          await new Promise(resolve => setTimeout(resolve, 100));

        } catch (err) {
          // Error is ok - server might close connection
        }
      }

      console.log(`Protocol violations handled: ${violationsHandled}/${protocolViolations.length}`);

      // Server should handle at least some violations properly
      expect(violationsHandled).toBeGreaterThan(0);

      if (socket.writable) {
        socket.write('QUIT\r\n');
      }
      socket.end();
      done.resolve();
    } catch (error) {
      socket.end();
      done.reject(error);
    }
  });

  socket.on('error', (error) => {
    done.reject(error);
  });
});

tap.test('ERR-07: Exception handling - Recovery after errors', async (tools) => {
  const done = tools.defer();
  const socket = net.createConnection({
    host: 'localhost',
    port: TEST_PORT,
    timeout: 30000
  });
  
  activeSockets.add(socket);
  socket.on('close', () => activeSockets.delete(socket));

  socket.on('connect', async () => {
    try {
      // Read greeting
      await new Promise<void>((resolve) => {
        socket.once('data', () => resolve());
      });

      // Send EHLO
      socket.write('EHLO testhost\r\n');
      
      await new Promise<void>((resolve) => {
        let data = '';
        const handleData = (chunk: Buffer) => {
          data += chunk.toString();
          if (data.includes('250 ') && data.includes('\r\n')) {
            socket.removeListener('data', handleData);
            resolve();
          }
        };
        socket.on('data', handleData);
      });

      // Trigger an error
      socket.write('INVALID_COMMAND\r\n');
      
      await new Promise<void>((resolve) => {
        socket.once('data', (chunk) => {
          const response = chunk.toString();
          expect(response).toMatch(/50[0-3]/);
          resolve();
        });
      });

      // Now try a valid command sequence to ensure recovery
      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');

      // Server recovered successfully after exception
      socket.write('RSET\r\n');
      
      const rsetResponse = await new Promise<string>((resolve) => {
        socket.once('data', (chunk) => {
          resolve(chunk.toString());
        });
      });

      expect(rsetResponse).toInclude('250');

      console.log('Server recovered successfully after exception');

      socket.write('QUIT\r\n');
      socket.end();
      done.resolve();
    } catch (error) {
      socket.end();
      done.reject(error);
    }
  });

  socket.on('error', (error) => {
    done.reject(error);
  });
});

tap.test('cleanup server', async () => {
  // Close any remaining sockets
  for (const socket of activeSockets) {
    if (!socket.destroyed) {
      socket.destroy();
    }
  }
  
  // Wait for all sockets to be fully closed
  await new Promise(resolve => setTimeout(resolve, 500));
  
  await stopTestServer(testServer);
});

export default tap.start();