import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createTestServer } from '../../helpers/server.loader.js';
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/index.js';

tap.test('CRFC-05: should comply with SMTP state machine (RFC 5321)', async (tools) => {
  const testId = 'CRFC-05-state-machine';
  console.log(`\n${testId}: Testing SMTP state machine compliance...`);

  let scenarioCount = 0;

  // Scenario 1: Initial state and greeting
  await (async () => {
    scenarioCount++;
    console.log(`\nScenario ${scenarioCount}: Testing initial state and greeting`);
    
    const testServer = await createTestServer({
      onConnection: async (socket) => {
        console.log('  [Server] Client connected - Initial state');
        
        let state = 'initial';
        
        // Send greeting immediately upon connection
        socket.write('220 statemachine.example.com ESMTP Service ready\r\n');
        state = 'greeting-sent';
        console.log('  [Server] State: initial -> greeting-sent');
        
        socket.on('data', (data) => {
          const command = data.toString().trim();
          console.log(`  [Server] State: ${state}, Received: ${command}`);
          
          if (state === 'greeting-sent') {
            if (command.startsWith('EHLO') || command.startsWith('HELO')) {
              socket.write('250 statemachine.example.com\r\n');
              state = 'ready';
              console.log('  [Server] State: greeting-sent -> ready');
            } else if (command === 'QUIT') {
              socket.write('221 Bye\r\n');
              socket.end();
            } else {
              socket.write('503 5.5.1 Bad sequence of commands\r\n');
            }
          } else if (state === 'ready') {
            if (command.startsWith('MAIL FROM:')) {
              socket.write('250 OK\r\n');
              state = 'mail';
              console.log('  [Server] State: ready -> mail');
            } else if (command.startsWith('EHLO') || command.startsWith('HELO')) {
              socket.write('250 statemachine.example.com\r\n');
              // Stay in ready state
            } else if (command === 'RSET' || command === 'NOOP') {
              socket.write('250 OK\r\n');
              // Stay in ready state
            } else if (command === 'QUIT') {
              socket.write('221 Bye\r\n');
              socket.end();
            } else {
              socket.write('503 5.5.1 Bad sequence of commands\r\n');
            }
          }
        });
      }
    });

    const smtpClient = createTestSmtpClient({
      host: testServer.hostname,
      port: testServer.port,
      secure: false
    });

    // Just establish connection and send EHLO
    try {
      await smtpClient.verify();
      console.log('  Initial state transition (connect -> EHLO) successful');
    } catch (error) {
      console.log(`  Connection/EHLO failed: ${error.message}`);
    }

    await testServer.server.close();
  })();

  // Scenario 2: Transaction state machine
  await (async () => {
    scenarioCount++;
    console.log(`\nScenario ${scenarioCount}: Testing transaction state machine`);
    
    const testServer = await createTestServer({
      onConnection: async (socket) => {
        console.log('  [Server] Client connected');
        socket.write('220 statemachine.example.com ESMTP\r\n');
        
        let state = 'ready';
        
        socket.on('data', (data) => {
          const command = data.toString().trim();
          console.log(`  [Server] State: ${state}, Command: ${command}`);
          
          switch (state) {
            case 'ready':
              if (command.startsWith('EHLO')) {
                socket.write('250 statemachine.example.com\r\n');
                // Stay in ready
              } else if (command.startsWith('MAIL FROM:')) {
                socket.write('250 OK\r\n');
                state = 'mail';
                console.log('  [Server] State: ready -> mail');
              } else if (command === 'QUIT') {
                socket.write('221 Bye\r\n');
                socket.end();
              } else {
                socket.write('503 5.5.1 Bad sequence of commands\r\n');
              }
              break;
              
            case 'mail':
              if (command.startsWith('RCPT TO:')) {
                socket.write('250 OK\r\n');
                state = 'rcpt';
                console.log('  [Server] State: mail -> rcpt');
              } else if (command === 'RSET') {
                socket.write('250 OK\r\n');
                state = 'ready';
                console.log('  [Server] State: mail -> ready (RSET)');
              } else if (command === 'QUIT') {
                socket.write('221 Bye\r\n');
                socket.end();
              } else {
                socket.write('503 5.5.1 Bad sequence of commands\r\n');
              }
              break;
              
            case 'rcpt':
              if (command.startsWith('RCPT TO:')) {
                socket.write('250 OK\r\n');
                // Stay in rcpt (can have multiple recipients)
              } else if (command === 'DATA') {
                socket.write('354 Start mail input\r\n');
                state = 'data';
                console.log('  [Server] State: rcpt -> data');
              } else if (command === 'RSET') {
                socket.write('250 OK\r\n');
                state = 'ready';
                console.log('  [Server] State: rcpt -> ready (RSET)');
              } else if (command === 'QUIT') {
                socket.write('221 Bye\r\n');
                socket.end();
              } else {
                socket.write('503 5.5.1 Bad sequence of commands\r\n');
              }
              break;
              
            case 'data':
              if (command === '.') {
                socket.write('250 OK\r\n');
                state = 'ready';
                console.log('  [Server] State: data -> ready (message complete)');
              } else if (command === 'QUIT') {
                // QUIT is not allowed during DATA
                socket.write('503 5.5.1 Bad sequence of commands\r\n');
              }
              // All other input during DATA is message content
              break;
          }
        });
      }
    });

    const smtpClient = createTestSmtpClient({
      host: testServer.hostname,
      port: testServer.port,
      secure: false
    });

    const email = new Email({
      from: 'sender@example.com',
      to: ['recipient1@example.com', 'recipient2@example.com'],
      subject: 'State machine test',
      text: 'Testing SMTP transaction state machine'
    });

    const result = await smtpClient.sendMail(email);
    console.log('  Complete transaction state sequence successful');
    expect(result).toBeDefined();
    expect(result.messageId).toBeDefined();

    await testServer.server.close();
  })();

  // Scenario 3: Invalid state transitions
  await (async () => {
    scenarioCount++;
    console.log(`\nScenario ${scenarioCount}: Testing invalid state transitions`);
    
    const testServer = await createTestServer({
      onConnection: async (socket) => {
        console.log('  [Server] Client connected');
        socket.write('220 statemachine.example.com ESMTP\r\n');
        
        let state = 'ready';
        
        socket.on('data', (data) => {
          const command = data.toString().trim();
          console.log(`  [Server] State: ${state}, Command: ${command}`);
          
          // Strictly enforce state machine
          switch (state) {
            case 'ready':
              if (command.startsWith('EHLO') || command.startsWith('HELO')) {
                socket.write('250 statemachine.example.com\r\n');
              } else if (command.startsWith('MAIL FROM:')) {
                socket.write('250 OK\r\n');
                state = 'mail';
              } else if (command === 'RSET' || command === 'NOOP') {
                socket.write('250 OK\r\n');
              } else if (command === 'QUIT') {
                socket.write('221 Bye\r\n');
                socket.end();
              } else if (command.startsWith('RCPT TO:')) {
                console.log('  [Server] RCPT TO without MAIL FROM');
                socket.write('503 5.5.1 Need MAIL command first\r\n');
              } else if (command === 'DATA') {
                console.log('  [Server] DATA without MAIL FROM and RCPT TO');
                socket.write('503 5.5.1 Need MAIL and RCPT commands first\r\n');
              } else {
                socket.write('503 5.5.1 Bad sequence of commands\r\n');
              }
              break;
              
            case 'mail':
              if (command.startsWith('RCPT TO:')) {
                socket.write('250 OK\r\n');
                state = 'rcpt';
              } else if (command.startsWith('MAIL FROM:')) {
                console.log('  [Server] Second MAIL FROM without RSET');
                socket.write('503 5.5.1 Sender already specified\r\n');
              } else if (command === 'DATA') {
                console.log('  [Server] DATA without RCPT TO');
                socket.write('503 5.5.1 Need RCPT command first\r\n');
              } else if (command === 'RSET') {
                socket.write('250 OK\r\n');
                state = 'ready';
              } else if (command === 'QUIT') {
                socket.write('221 Bye\r\n');
                socket.end();
              } else {
                socket.write('503 5.5.1 Bad sequence of commands\r\n');
              }
              break;
              
            case 'rcpt':
              if (command.startsWith('RCPT TO:')) {
                socket.write('250 OK\r\n');
              } else if (command === 'DATA') {
                socket.write('354 Start mail input\r\n');
                state = 'data';
              } else if (command.startsWith('MAIL FROM:')) {
                console.log('  [Server] MAIL FROM after RCPT TO without RSET');
                socket.write('503 5.5.1 Sender already specified\r\n');
              } else if (command === 'RSET') {
                socket.write('250 OK\r\n');
                state = 'ready';
              } else if (command === 'QUIT') {
                socket.write('221 Bye\r\n');
                socket.end();
              } else {
                socket.write('503 5.5.1 Bad sequence of commands\r\n');
              }
              break;
              
            case 'data':
              if (command === '.') {
                socket.write('250 OK\r\n');
                state = 'ready';
              } else if (command.startsWith('MAIL FROM:') || 
                        command.startsWith('RCPT TO:') || 
                        command === 'RSET') {
                console.log('  [Server] SMTP command during DATA mode');
                socket.write('503 5.5.1 Commands not allowed during data transfer\r\n');
              }
              // During DATA, most input is treated as message content
              break;
          }
        });
      }
    });

    // We'll create a custom client to send invalid command sequences
    const testCases = [
      {
        name: 'RCPT without MAIL',
        commands: ['EHLO client.example.com', 'RCPT TO:<test@example.com>'],
        expectError: true
      },
      {
        name: 'DATA without RCPT',
        commands: ['EHLO client.example.com', 'MAIL FROM:<sender@example.com>', 'DATA'],
        expectError: true
      },
      {
        name: 'Double MAIL FROM',
        commands: ['EHLO client.example.com', 'MAIL FROM:<sender1@example.com>', 'MAIL FROM:<sender2@example.com>'],
        expectError: true
      }
    ];

    for (const testCase of testCases) {
      console.log(`  Testing: ${testCase.name}`);
      
      try {
        // Create simple socket connection for manual command testing
        const net = await import('net');
        const client = net.createConnection(testServer.port, testServer.hostname);
        
        let responseCount = 0;
        let errorReceived = false;
        
        client.on('data', (data) => {
          const response = data.toString();
          console.log(`    Response: ${response.trim()}`);
          
          if (response.startsWith('5')) {
            errorReceived = true;
          }
          
          responseCount++;
          
          if (responseCount <= testCase.commands.length) {
            const command = testCase.commands[responseCount - 1];
            if (command) {
              setTimeout(() => {
                console.log(`    Sending: ${command}`);
                client.write(command + '\r\n');
              }, 100);
            }
          } else {
            client.write('QUIT\r\n');
            client.end();
          }
        });
        
        await new Promise((resolve, reject) => {
          client.on('end', () => {
            if (testCase.expectError && errorReceived) {
              console.log(`    ✓ Expected error received`);
            } else if (!testCase.expectError && !errorReceived) {
              console.log(`    ✓ No error as expected`);
            } else {
              console.log(`    ✗ Unexpected result`);
            }
            resolve(void 0);
          });
          
          client.on('error', reject);
          
          // Start with greeting response
          setTimeout(() => {
            if (testCase.commands.length > 0) {
              console.log(`    Sending: ${testCase.commands[0]}`);
              client.write(testCase.commands[0] + '\r\n');
            }
          }, 100);
        });
        
      } catch (error) {
        console.log(`    Error testing ${testCase.name}: ${error.message}`);
      }
    }

    await testServer.server.close();
  })();

  // Scenario 4: RSET command state transitions
  await (async () => {
    scenarioCount++;
    console.log(`\nScenario ${scenarioCount}: Testing RSET command state transitions`);
    
    const testServer = await createTestServer({
      onConnection: async (socket) => {
        console.log('  [Server] Client connected');
        socket.write('220 statemachine.example.com ESMTP\r\n');
        
        let state = 'ready';
        
        socket.on('data', (data) => {
          const command = data.toString().trim();
          console.log(`  [Server] State: ${state}, Command: ${command}`);
          
          if (command.startsWith('EHLO')) {
            socket.write('250 statemachine.example.com\r\n');
            state = 'ready';
          } else if (command.startsWith('MAIL FROM:')) {
            socket.write('250 OK\r\n');
            state = 'mail';
          } else if (command.startsWith('RCPT TO:')) {
            if (state === 'mail' || state === 'rcpt') {
              socket.write('250 OK\r\n');
              state = 'rcpt';
            } else {
              socket.write('503 5.5.1 Bad sequence of commands\r\n');
            }
          } else if (command === 'RSET') {
            console.log(`  [Server] RSET from state: ${state} -> ready`);
            socket.write('250 OK\r\n');
            state = 'ready';
          } else if (command === 'DATA') {
            if (state === 'rcpt') {
              socket.write('354 Start mail input\r\n');
              state = 'data';
            } else {
              socket.write('503 5.5.1 Bad sequence of commands\r\n');
            }
          } else if (command === '.') {
            if (state === 'data') {
              socket.write('250 OK\r\n');
              state = 'ready';
            }
          } else if (command === 'QUIT') {
            socket.write('221 Bye\r\n');
            socket.end();
          } else if (command === 'NOOP') {
            socket.write('250 OK\r\n');
          }
        });
      }
    });

    const smtpClient = createTestSmtpClient({
      host: testServer.hostname,
      port: testServer.port,
      secure: false
    });

    // Test RSET at various points in transaction
    console.log('  Testing RSET from different states...');
    
    // We'll manually test RSET behavior
    const net = await import('net');
    const client = net.createConnection(testServer.port, testServer.hostname);
    
    const commands = [
      'EHLO client.example.com',           // -> ready
      'MAIL FROM:<sender@example.com>',    // -> mail
      'RSET',                              // -> ready (reset from mail state)
      'MAIL FROM:<sender2@example.com>',   // -> mail
      'RCPT TO:<rcpt1@example.com>',       // -> rcpt
      'RCPT TO:<rcpt2@example.com>',       // -> rcpt (multiple recipients)
      'RSET',                              // -> ready (reset from rcpt state)
      'MAIL FROM:<sender3@example.com>',   // -> mail (fresh transaction)
      'RCPT TO:<rcpt3@example.com>',       // -> rcpt
      'DATA',                              // -> data
      '.',                                 // -> ready (complete transaction)
      'QUIT'
    ];
    
    let commandIndex = 0;
    
    client.on('data', (data) => {
      const response = data.toString().trim();
      console.log(`    Response: ${response}`);
      
      if (commandIndex < commands.length) {
        setTimeout(() => {
          const command = commands[commandIndex];
          console.log(`    Sending: ${command}`);
          if (command === 'DATA') {
            client.write(command + '\r\n');
            // Send message content immediately after DATA
            setTimeout(() => {
              client.write('Subject: RSET test\r\n\r\nTesting RSET state transitions.\r\n.\r\n');
            }, 100);
          } else {
            client.write(command + '\r\n');
          }
          commandIndex++;
        }, 100);
      } else {
        client.end();
      }
    });
    
    await new Promise((resolve, reject) => {
      client.on('end', () => {
        console.log('  RSET state transitions completed successfully');
        resolve(void 0);
      });
      client.on('error', reject);
    });

    await testServer.server.close();
  })();

  // Scenario 5: Connection state persistence
  await (async () => {
    scenarioCount++;
    console.log(`\nScenario ${scenarioCount}: Testing connection state persistence`);
    
    const testServer = await createTestServer({
      onConnection: async (socket) => {
        console.log('  [Server] Client connected');
        socket.write('220 statemachine.example.com ESMTP\r\n');
        
        let state = 'ready';
        let messageCount = 0;
        
        socket.on('data', (data) => {
          const command = data.toString().trim();
          
          if (command.startsWith('EHLO')) {
            socket.write('250-statemachine.example.com\r\n');
            socket.write('250 PIPELINING\r\n');
            state = 'ready';
          } else if (command.startsWith('MAIL FROM:')) {
            if (state === 'ready') {
              socket.write('250 OK\r\n');
              state = 'mail';
            } else {
              socket.write('503 5.5.1 Bad sequence\r\n');
            }
          } else if (command.startsWith('RCPT TO:')) {
            if (state === 'mail' || state === 'rcpt') {
              socket.write('250 OK\r\n');
              state = 'rcpt';
            } else {
              socket.write('503 5.5.1 Bad sequence\r\n');
            }
          } else if (command === 'DATA') {
            if (state === 'rcpt') {
              socket.write('354 Start mail input\r\n');
              state = 'data';
            } else {
              socket.write('503 5.5.1 Bad sequence\r\n');
            }
          } else if (command === '.') {
            if (state === 'data') {
              messageCount++;
              console.log(`  [Server] Message ${messageCount} completed`);
              socket.write(`250 OK: Message ${messageCount} accepted\r\n`);
              state = 'ready';
            }
          } else if (command === 'QUIT') {
            console.log(`  [Server] Session ended after ${messageCount} messages`);
            socket.write('221 Bye\r\n');
            socket.end();
          }
        });
      }
    });

    const smtpClient = createTestSmtpClient({
      host: testServer.hostname,
      port: testServer.port,
      secure: false,
      pool: true,
      maxConnections: 1
    });

    // Send multiple emails through same connection
    for (let i = 1; i <= 3; i++) {
      const email = new Email({
        from: 'sender@example.com',
        to: [`recipient${i}@example.com`],
        subject: `Persistence test ${i}`,
        text: `Testing connection state persistence - message ${i}`
      });

      const result = await smtpClient.sendMail(email);
      console.log(`  Message ${i} sent successfully`);
      expect(result).toBeDefined();
      expect(result.response).toContain(`Message ${i}`);
    }

    // Close the pooled connection
    await smtpClient.close();
    await testServer.server.close();
  })();

  // Scenario 6: Error state recovery
  await (async () => {
    scenarioCount++;
    console.log(`\nScenario ${scenarioCount}: Testing error state recovery`);
    
    const testServer = await createTestServer({
      onConnection: async (socket) => {
        console.log('  [Server] Client connected');
        socket.write('220 statemachine.example.com ESMTP\r\n');
        
        let state = 'ready';
        let errorCount = 0;
        
        socket.on('data', (data) => {
          const command = data.toString().trim();
          console.log(`  [Server] State: ${state}, Command: ${command}`);
          
          if (command.startsWith('EHLO')) {
            socket.write('250 statemachine.example.com\r\n');
            state = 'ready';
            errorCount = 0;  // Reset error count on new session
          } else if (command.startsWith('MAIL FROM:')) {
            const address = command.match(/<(.+)>/)?.[1] || '';
            if (address.includes('error')) {
              errorCount++;
              console.log(`  [Server] Error ${errorCount} - invalid sender`);
              socket.write('550 5.1.8 Invalid sender address\r\n');
              // State remains ready after error
            } else {
              socket.write('250 OK\r\n');
              state = 'mail';
            }
          } else if (command.startsWith('RCPT TO:')) {
            if (state === 'mail' || state === 'rcpt') {
              const address = command.match(/<(.+)>/)?.[1] || '';
              if (address.includes('error')) {
                errorCount++;
                console.log(`  [Server] Error ${errorCount} - invalid recipient`);
                socket.write('550 5.1.1 User unknown\r\n');
                // State remains the same after recipient error
              } else {
                socket.write('250 OK\r\n');
                state = 'rcpt';
              }
            } else {
              socket.write('503 5.5.1 Bad sequence\r\n');
            }
          } else if (command === 'DATA') {
            if (state === 'rcpt') {
              socket.write('354 Start mail input\r\n');
              state = 'data';
            } else {
              socket.write('503 5.5.1 Bad sequence\r\n');
            }
          } else if (command === '.') {
            if (state === 'data') {
              socket.write('250 OK\r\n');
              state = 'ready';
            }
          } else if (command === 'RSET') {
            console.log(`  [Server] RSET - recovering from errors (${errorCount} errors so far)`);
            socket.write('250 OK\r\n');
            state = 'ready';
          } else if (command === 'QUIT') {
            console.log(`  [Server] Session ended with ${errorCount} total errors`);
            socket.write('221 Bye\r\n');
            socket.end();
          } else {
            socket.write('500 5.5.1 Command not recognized\r\n');
          }
        });
      }
    });

    const smtpClient = createTestSmtpClient({
      host: testServer.hostname,
      port: testServer.port,
      secure: false
    });

    // Test recovery from various errors
    const testEmails = [
      {
        from: 'error@example.com',  // Will cause sender error
        to: ['valid@example.com'],
        desc: 'invalid sender'
      },
      {
        from: 'valid@example.com',
        to: ['error@example.com', 'valid@example.com'],  // Mixed valid/invalid recipients
        desc: 'mixed recipients'
      },
      {
        from: 'valid@example.com',
        to: ['valid@example.com'],
        desc: 'valid email after errors'
      }
    ];

    for (const testEmail of testEmails) {
      console.log(`  Testing ${testEmail.desc}...`);
      
      const email = new Email({
        from: testEmail.from,
        to: testEmail.to,
        subject: `Error recovery test: ${testEmail.desc}`,
        text: `Testing error state recovery with ${testEmail.desc}`
      });

      try {
        const result = await smtpClient.sendMail(email);
        console.log(`    ${testEmail.desc}: Success`);
        if (result.rejected && result.rejected.length > 0) {
          console.log(`    Rejected: ${result.rejected.length} recipients`);
        }
      } catch (error) {
        console.log(`    ${testEmail.desc}: Failed as expected - ${error.message}`);
      }
    }

    await testServer.server.close();
  })();

  console.log(`\n${testId}: All ${scenarioCount} state machine scenarios tested ✓`);
});

tap.start();