import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { PortProxy } from '../ts/classes.portproxy.js';

let testServer: net.Server;
let portProxy: PortProxy;
const TEST_SERVER_PORT = 4000;
const PROXY_PORT = 4001;
const TEST_DATA = 'Hello through port proxy!';

// Track all created servers and proxies for proper cleanup
const allServers: net.Server[] = [];
const allProxies: PortProxy[] = [];

// Helper: Creates a test TCP server that listens on a given port and host.
function createTestServer(port: number, host: string = 'localhost'): Promise<net.Server> {
  return new Promise((resolve) => {
    const server = net.createServer((socket) => {
      socket.on('data', (data) => {
        // Echo the received data back with a prefix.
        socket.write(`Echo: ${data.toString()}`);
      });
      socket.on('error', (error) => {
        console.error(`[Test Server] Socket error on ${host}:${port}:`, error);
      });
    });
    server.listen(port, host, () => {
      console.log(`[Test Server] Listening on ${host}:${port}`);
      allServers.push(server); // Track this server
      resolve(server);
    });
  });
}

// Helper: Creates a test client connection.
function createTestClient(port: number, data: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const client = new net.Socket();
    let response = '';
    
    const timeout = setTimeout(() => {
      client.destroy();
      reject(new Error(`Client connection timeout to port ${port}`));
    }, 5000);
    
    client.connect(port, 'localhost', () => {
      console.log('[Test Client] Connected to server');
      client.write(data);
    });
    client.on('data', (chunk) => {
      response += chunk.toString();
      client.end();
    });
    client.on('end', () => {
      clearTimeout(timeout);
      resolve(response);
    });
    client.on('error', (error) => {
      clearTimeout(timeout);
      reject(error);
    });
  });
}

// SETUP: Create a test server and a PortProxy instance.
tap.test('setup port proxy test environment', async () => {
  testServer = await createTestServer(TEST_SERVER_PORT);
  portProxy = new PortProxy({
    fromPort: PROXY_PORT,
    toPort: TEST_SERVER_PORT,
    targetIP: 'localhost',
    domainConfigs: [],
    sniEnabled: false,
    defaultAllowedIPs: ['127.0.0.1'],
    globalPortRanges: []
  });
  allProxies.push(portProxy); // Track this proxy
});

// Test that the proxy starts and its servers are listening.
tap.test('should start port proxy', async () => {
  await portProxy.start();
  expect((portProxy as any).netServers.every((server: net.Server) => server.listening)).toBeTrue();
});

// Test basic TCP forwarding.
tap.test('should forward TCP connections and data to localhost', async () => {
  const response = await createTestClient(PROXY_PORT, TEST_DATA);
  expect(response).toEqual(`Echo: ${TEST_DATA}`);
});

// Test proxy with a custom target host.
tap.test('should forward TCP connections to custom host', async () => {
  const customHostProxy = new PortProxy({
    fromPort: PROXY_PORT + 1,
    toPort: TEST_SERVER_PORT,
    targetIP: '127.0.0.1',
    domainConfigs: [],
    sniEnabled: false,
    defaultAllowedIPs: ['127.0.0.1'],
    globalPortRanges: []
  });
  allProxies.push(customHostProxy); // Track this proxy
  
  await customHostProxy.start();
  const response = await createTestClient(PROXY_PORT + 1, TEST_DATA);
  expect(response).toEqual(`Echo: ${TEST_DATA}`);
  await customHostProxy.stop();
  
  // Remove from tracking after stopping
  const index = allProxies.indexOf(customHostProxy);
  if (index !== -1) allProxies.splice(index, 1);
});

// Test custom IP forwarding
// Modified to work in Docker/CI environments without needing 127.0.0.2
tap.test('should forward connections to custom IP', async () => {
  // Set up ports that are FAR apart to avoid any possible confusion
  const forcedProxyPort = PROXY_PORT + 2;      // 4003 - The port that our proxy listens on
  const targetServerPort = TEST_SERVER_PORT + 200;  // 4200 - Target test server on different port
  
  // Create a test server listening on a unique port on 127.0.0.1 (works in all environments)
  const testServer2 = await createTestServer(targetServerPort, '127.0.0.1');

  // We're simulating routing to a different IP by using a different port
  // This tests the core functionality without requiring multiple IPs
  const domainProxy = new PortProxy({
    fromPort: forcedProxyPort,  // 4003 - Listen on this port
    toPort: targetServerPort,   // 4200 - Forward to this port
    targetIP: '127.0.0.1',      // Always use localhost (works in Docker)
    domainConfigs: [],          // No domain configs to confuse things
    sniEnabled: false,
    defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], // Allow localhost
    // We'll test the functionality WITHOUT port ranges this time
    globalPortRanges: []
  });
  allProxies.push(domainProxy); // Track this proxy

  await domainProxy.start();

  // Send a single test connection
  const response = await createTestClient(forcedProxyPort, TEST_DATA);
  expect(response).toEqual(`Echo: ${TEST_DATA}`);

  await domainProxy.stop();
  
  // Remove from tracking after stopping
  const proxyIndex = allProxies.indexOf(domainProxy);
  if (proxyIndex !== -1) allProxies.splice(proxyIndex, 1);
  
  // Close the test server
  await new Promise<void>((resolve) => testServer2.close(() => resolve()));
  
  // Remove from tracking
  const serverIndex = allServers.indexOf(testServer2);
  if (serverIndex !== -1) allServers.splice(serverIndex, 1);
});

// Test handling of multiple concurrent connections.
tap.test('should handle multiple concurrent connections', async () => {
  const concurrentRequests = 5;
  const requests = Array(concurrentRequests).fill(null).map((_, i) =>
    createTestClient(PROXY_PORT, `${TEST_DATA} ${i + 1}`)
  );
  const responses = await Promise.all(requests);
  responses.forEach((response, i) => {
    expect(response).toEqual(`Echo: ${TEST_DATA} ${i + 1}`);
  });
});

// Test connection timeout handling.
tap.test('should handle connection timeouts', async () => {
  const client = new net.Socket();
  await new Promise<void>((resolve) => {
    // Add a timeout to ensure we don't hang here
    const timeout = setTimeout(() => {
      client.destroy();
      resolve();
    }, 3000);
    
    client.connect(PROXY_PORT, 'localhost', () => {
      // Do not send any data to trigger a timeout.
      client.on('close', () => {
        clearTimeout(timeout);
        resolve();
      });
    });
    
    client.on('error', () => {
      clearTimeout(timeout);
      client.destroy();
      resolve();
    });
  });
});

// Test stopping the port proxy.
tap.test('should stop port proxy', async () => {
  await portProxy.stop();
  expect((portProxy as any).netServers.every((server: net.Server) => !server.listening)).toBeTrue();
  
  // Remove from tracking
  const index = allProxies.indexOf(portProxy);
  if (index !== -1) allProxies.splice(index, 1);
});

// Test chained proxies with and without source IP preservation.
tap.test('should support optional source IP preservation in chained proxies', async () => {
  // Chained proxies without IP preservation.
  const firstProxyDefault = new PortProxy({
    fromPort: PROXY_PORT + 4,
    toPort: PROXY_PORT + 5,
    targetIP: 'localhost',
    domainConfigs: [],
    sniEnabled: false,
    defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
    globalPortRanges: []
  });
  const secondProxyDefault = new PortProxy({
    fromPort: PROXY_PORT + 5,
    toPort: TEST_SERVER_PORT,
    targetIP: 'localhost',
    domainConfigs: [],
    sniEnabled: false,
    defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
    globalPortRanges: []
  });
  
  allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies
  
  await secondProxyDefault.start();
  await firstProxyDefault.start();
  const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA);
  expect(response1).toEqual(`Echo: ${TEST_DATA}`);
  await firstProxyDefault.stop();
  await secondProxyDefault.stop();
  
  // Remove from tracking
  const index1 = allProxies.indexOf(firstProxyDefault);
  if (index1 !== -1) allProxies.splice(index1, 1);
  const index2 = allProxies.indexOf(secondProxyDefault);
  if (index2 !== -1) allProxies.splice(index2, 1);

  // Chained proxies with IP preservation.
  const firstProxyPreserved = new PortProxy({
    fromPort: PROXY_PORT + 6,
    toPort: PROXY_PORT + 7,
    targetIP: 'localhost',
    domainConfigs: [],
    sniEnabled: false,
    defaultAllowedIPs: ['127.0.0.1'],
    preserveSourceIP: true,
    globalPortRanges: []
  });
  const secondProxyPreserved = new PortProxy({
    fromPort: PROXY_PORT + 7,
    toPort: TEST_SERVER_PORT,
    targetIP: 'localhost',
    domainConfigs: [],
    sniEnabled: false,
    defaultAllowedIPs: ['127.0.0.1'],
    preserveSourceIP: true,
    globalPortRanges: []
  });
  
  allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies
  
  await secondProxyPreserved.start();
  await firstProxyPreserved.start();
  const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA);
  expect(response2).toEqual(`Echo: ${TEST_DATA}`);
  await firstProxyPreserved.stop();
  await secondProxyPreserved.stop();
  
  // Remove from tracking
  const index3 = allProxies.indexOf(firstProxyPreserved);
  if (index3 !== -1) allProxies.splice(index3, 1);
  const index4 = allProxies.indexOf(secondProxyPreserved);
  if (index4 !== -1) allProxies.splice(index4, 1);
});

// Test round-robin behavior for multiple target IPs in a domain config.
tap.test('should use round robin for multiple target IPs in domain config', async () => {
  const domainConfig = {
    domains: ['rr.test'],
    allowedIPs: ['127.0.0.1'],
    targetIPs: ['hostA', 'hostB']
  } as any;
  
  const proxyInstance = new PortProxy({
    fromPort: 0,
    toPort: 0,
    targetIP: 'localhost',
    domainConfigs: [domainConfig],
    sniEnabled: false,
    defaultAllowedIPs: [],
    globalPortRanges: []
  });
  
  // Don't track this proxy as it doesn't actually start or listen
  
  const firstTarget = (proxyInstance as any).getTargetIP(domainConfig);
  const secondTarget = (proxyInstance as any).getTargetIP(domainConfig);
  expect(firstTarget).toEqual('hostA');
  expect(secondTarget).toEqual('hostB');
});

// CLEANUP: Tear down all servers and proxies
tap.test('cleanup port proxy test environment', async () => {
  // Stop all remaining proxies
  for (const proxy of [...allProxies]) {
    try {
      await proxy.stop();
      const index = allProxies.indexOf(proxy);
      if (index !== -1) allProxies.splice(index, 1);
    } catch (err) {
      console.error(`Error stopping proxy: ${err}`);
    }
  }
  
  // Close all remaining servers
  for (const server of [...allServers]) {
    try {
      await new Promise<void>((resolve) => {
        if (server.listening) {
          server.close(() => resolve());
        } else {
          resolve();
        }
      });
      const index = allServers.indexOf(server);
      if (index !== -1) allServers.splice(index, 1);
    } catch (err) {
      console.error(`Error closing server: ${err}`);
    }
  }
  
  // Verify all resources are cleaned up
  expect(allProxies.length).toEqual(0);
  expect(allServers.length).toEqual(0);
});

export default tap.start();