import { test } from '@japa/runner'
import net from 'net'
import {
  ConnectionManager,
  ConnectionOptions,
  ProxyConnectionOptions,
  TunnelConnectionOptions,
} from '../../../../../src/runtime/http-engine/connections/ConnectionManager.js'
import { DirectConnection } from '../../../../../src/runtime/http-engine/connections/DirectConnection.js'
import { ProxyConnection } from '../../../../../src/runtime/http-engine/connections/ProxyConnection.js'
import { TunnelConnection } from '../../../../../src/runtime/http-engine/connections/TunnelConnection.js'
import { createLogger, type Logger, type ILogObj } from '../../../../../src/lib/logging/index.js'
import { type RequestStats, type HttpEngineOptions } from '../../../../../src/runtime/http-engine/CoreEngine.js'
import { HttpCertificate } from '../../../../../src/models/ClientCertificate.js'

test.group('ConnectionManager', (group) => {
  let connectionManager: ConnectionManager
  let logger: Logger<ILogObj>
  let stats: RequestStats
  let createdSockets: net.Socket[] = []

  group.each.setup(() => {
    logger = createLogger()
    stats = {}
    connectionManager = new ConnectionManager(logger, stats)
    createdSockets = []
  })

  group.each.teardown(() => {
    // Clean up any sockets created during tests
    for (const socket of createdSockets) {
      if (!socket.destroyed) {
        socket.destroy()
      }
    }
    createdSockets = []
  })

  test('createDirectConnection should return DirectConnection instance', ({ assert }) => {
    const options = {
      host: 'example.com',
      port: 80,
      protocol: 'http:',
      logger,
      stats,
    }

    const connection = connectionManager.createDirectConnection(options)
    assert.instanceOf(connection, DirectConnection, 'creates DirectConnection instance')
  })

  test('createProxyConnection should return ProxyConnection instance', ({ assert }) => {
    const options = {
      host: 'example.com',
      port: 80,
      protocol: 'http:',
      proxy: '127.0.0.1:8080',
      proxyIsSsl: false,
      targetUrl: 'http://example.com:80',
      logger,
      stats,
    }

    const connection = connectionManager.createProxyConnection(options)
    assert.instanceOf(connection, ProxyConnection, 'creates ProxyConnection instance')
  })

  test('createTunnelConnection should return TunnelConnection instance', ({ assert }) => {
    const options = {
      host: 'example.com',
      port: 443,
      protocol: 'https:',
      proxy: '127.0.0.1:8080',
      proxyIsSsl: false,
      targetUrl: 'https://example.com:443',
      targetHost: 'example.com',
      targetPort: 443,
      logger,
      stats,
    }

    const connection = connectionManager.createTunnelConnection(options)
    assert.instanceOf(connection, TunnelConnection, 'creates TunnelConnection instance')
  })

  test('should create direct connection when no proxy is configured', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)

    // Mock DirectConnection.connect to return our mock socket
    const originalCreateDirectConnection = connectionManager.createDirectConnection
    let capturedOptions: ConnectionOptions | undefined

    connectionManager.createDirectConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as DirectConnection
    }

    const engineOptions: HttpEngineOptions = {
      timeout: 5000,
      validateCertificates: true,
    }

    const result = await connectionManager.createConnection('example.com', 80, 'http:', engineOptions)

    assert.deepEqual(result, mockSocket, 'returns the socket from DirectConnection')
    assert.equal(capturedOptions!.host, 'example.com', 'passes correct host')
    assert.equal(capturedOptions!.port, 80, 'passes correct port')
    assert.equal(capturedOptions!.protocol, 'http:', 'passes correct protocol')
    assert.equal(capturedOptions!.timeout, 5000, 'passes timeout option')
    assert.equal(capturedOptions!.validateCertificates, true, 'passes certificate validation option')
    assert.equal(capturedOptions!.logger, logger, 'passes logger')
    assert.equal(capturedOptions!.stats, stats, 'passes stats')

    // Restore original method
    connectionManager.createDirectConnection = originalCreateDirectConnection
  })

  test('should create proxy connection for HTTP through proxy', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)

    // Mock ProxyConnection.connect to return our mock socket
    const originalCreateProxyConnection = connectionManager.createProxyConnection
    let capturedOptions: ProxyConnectionOptions | undefined

    connectionManager.createProxyConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as ProxyConnection
    }

    const engineOptions: HttpEngineOptions = {
      proxy: '127.0.0.1:8080',
      timeout: 5000,
      validateCertificates: false,
      proxyUsername: 'user',
      proxyPassword: 'pass',
      proxyAuthorization: 'Basic',
    }

    const result = await connectionManager.createConnection('example.com', 80, 'http:', engineOptions)

    assert.equal(result, mockSocket, 'returns the socket from ProxyConnection')
    assert.equal(capturedOptions!.host, 'example.com', 'passes correct host')
    assert.equal(capturedOptions!.port, 80, 'passes correct port')
    assert.equal(capturedOptions!.protocol, 'http:', 'passes correct protocol')
    assert.equal(capturedOptions!.proxy, '127.0.0.1:8080', 'passes proxy configuration')
    assert.equal(capturedOptions!.proxyIsSsl, false, 'detects non-SSL proxy')
    assert.equal(capturedOptions!.targetUrl, 'http://example.com:80', 'constructs target URL')
    assert.equal(capturedOptions!.proxyUsername, 'user', 'passes proxy username')
    assert.equal(capturedOptions!.proxyPassword, 'pass', 'passes proxy password')
    assert.equal(capturedOptions!.proxyAuthorization, 'Basic', 'passes proxy authorization')

    // Restore original method
    connectionManager.createProxyConnection = originalCreateProxyConnection
  })

  test('should create tunnel connection for HTTPS through proxy', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)

    // Mock TunnelConnection.connect to return our mock socket
    const originalCreateTunnelConnection = connectionManager.createTunnelConnection
    let capturedOptions: TunnelConnectionOptions | undefined

    connectionManager.createTunnelConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as TunnelConnection
    }

    const engineOptions: HttpEngineOptions = {
      proxy: 'https://proxy.example.com:8443',
      timeout: 10000,
      validateCertificates: true,
      proxyUsername: 'admin',
      proxyPassword: 'secret',
    }

    const result = await connectionManager.createConnection('secure.example.com', 443, 'https:', engineOptions)

    assert.equal(result, mockSocket, 'returns the socket from TunnelConnection')
    assert.equal(capturedOptions!.host, 'secure.example.com', 'passes correct host')
    assert.equal(capturedOptions!.port, 443, 'passes correct port')
    assert.equal(capturedOptions!.protocol, 'https:', 'passes correct protocol')
    assert.equal(capturedOptions!.proxy, 'https://proxy.example.com:8443', 'passes proxy configuration')
    assert.equal(capturedOptions!.proxyIsSsl, true, 'detects SSL proxy')
    assert.equal(capturedOptions!.targetUrl, 'https://secure.example.com:443', 'constructs target URL')
    assert.equal(capturedOptions!.targetHost, 'secure.example.com', 'passes target host')
    assert.equal(capturedOptions!.targetPort, 443, 'passes target port')
    assert.equal(capturedOptions!.proxyUsername, 'admin', 'passes proxy username')
    assert.equal(capturedOptions!.proxyPassword, 'secret', 'passes proxy password')

    // Restore original method
    connectionManager.createTunnelConnection = originalCreateTunnelConnection
  })

  test('should detect SSL proxy correctly with https: scheme', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)

    const originalCreateProxyConnection = connectionManager.createProxyConnection
    let capturedOptions: ProxyConnectionOptions | undefined

    connectionManager.createProxyConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as ProxyConnection
    }

    const engineOptions: HttpEngineOptions = {
      proxy: 'https://secure-proxy.example.com:8443',
    }

    await connectionManager.createConnection('example.com', 80, 'http:', engineOptions)

    assert.equal(capturedOptions!.proxyIsSsl, true, 'correctly identifies SSL proxy')

    // Restore original method
    connectionManager.createProxyConnection = originalCreateProxyConnection
  })

  test('should detect non-SSL proxy correctly without https: scheme', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)

    const originalCreateProxyConnection = connectionManager.createProxyConnection
    let capturedOptions: ProxyConnectionOptions | undefined

    connectionManager.createProxyConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as ProxyConnection
    }

    const engineOptions: HttpEngineOptions = {
      proxy: '192.168.1.100:3128',
    }

    const socket = await connectionManager.createConnection('example.com', 80, 'http:', engineOptions)
    createdSockets.push(socket)

    assert.equal(capturedOptions!.proxyIsSsl, false, 'correctly identifies non-SSL proxy')

    // Restore original method
    connectionManager.createProxyConnection = originalCreateProxyConnection
  })

  test('should pass client certificates to connection options', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)
    const mockCertificates: HttpCertificate[] = [
      {
        key: 'abcd1234',
        kind: 'Core#Certificate',
        name: 'test-cert',
        cert: { data: Buffer.from('cert-data') },
        certKey: { data: Buffer.from('key-data'), passphrase: 'secret' },
        type: 'pem',
      },
    ]

    const originalCreateDirectConnection = connectionManager.createDirectConnection
    let capturedOptions: ConnectionOptions | undefined

    connectionManager.createDirectConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as DirectConnection
    }

    const engineOptions: HttpEngineOptions = {
      certificates: mockCertificates,
      validateCertificates: false,
    }

    await connectionManager.createConnection('client-cert.example.com', 443, 'https:', engineOptions)

    assert.equal(capturedOptions!.certificates, mockCertificates, 'passes client certificates')
    assert.equal(capturedOptions!.validateCertificates, false, 'passes certificate validation setting')

    // Restore original method
    connectionManager.createDirectConnection = originalCreateDirectConnection
  })

  test('should handle undefined optional parameters', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)

    const originalCreateDirectConnection = connectionManager.createDirectConnection
    let capturedOptions: ConnectionOptions | undefined

    connectionManager.createDirectConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as DirectConnection
    }

    const engineOptions: HttpEngineOptions = {}

    await connectionManager.createConnection('example.com', 80, 'http:', engineOptions)

    assert.isUndefined(capturedOptions!.timeout, 'handles undefined timeout')
    assert.isUndefined(capturedOptions!.validateCertificates, 'handles undefined certificate validation')
    assert.isUndefined(capturedOptions!.certificates, 'handles undefined certificates')

    // Restore original method
    connectionManager.createDirectConnection = originalCreateDirectConnection
  })

  test('should handle custom ports correctly', async ({ assert }) => {
    const mockSocket = new net.Socket()
    createdSockets.push(mockSocket)

    const originalCreateProxyConnection = connectionManager.createProxyConnection
    let capturedOptions: ProxyConnectionOptions | undefined

    connectionManager.createProxyConnection = (options) => {
      capturedOptions = options
      return {
        connect: async () => mockSocket,
      } as ProxyConnection
    }

    const engineOptions: HttpEngineOptions = {
      proxy: '127.0.0.1:8080',
    }

    await connectionManager.createConnection('api.example.com', 8090, 'http:', engineOptions)

    assert.equal(capturedOptions!.targetUrl, 'http://api.example.com:8090', 'constructs URL with custom port')

    // Restore original method
    connectionManager.createProxyConnection = originalCreateProxyConnection
  })

  test('should create working connection through proxy', async ({ assert, httpConfig }) => {
    const engineOptions: HttpEngineOptions = {
      proxy: `127.0.0.1:${httpConfig.proxyHttpPort}`,
      timeout: 5000,
      validateCertificates: false,
    }

    // Try to create a real connection through the proxy
    const socket = await connectionManager.createConnection('httpbin.org', 80, 'http:', engineOptions)
    createdSockets.push(socket)

    assert.ok(socket, 'creates a real socket')
    assert.instanceOf(socket, net.Socket, 'returns a net.Socket instance')

    // Note: socket will be cleaned up in teardown
  }).timeout(10000)

  test('should create working HTTPS tunnel through proxy', async ({ assert, httpConfig }) => {
    const engineOptions: HttpEngineOptions = {
      proxy: `https://127.0.0.1:${httpConfig.proxyHttpsPort}`,
      timeout: 5000,
      validateCertificates: false,
    }

    // Try to create a real HTTPS tunnel through the proxy
    const socket = await connectionManager.createConnection('httpbin.org', 443, 'https:', engineOptions)
    createdSockets.push(socket)

    assert.ok(socket, 'creates a real socket')
    assert.instanceOf(socket, net.Socket, 'returns a net.Socket instance')

    // Note: socket will be cleaned up in teardown
  }).timeout(10000)
})
