'use strict';
var assert = require('chai').assert;
var sinon = require('sinon');
var assign = require('lodash/assign');
var pick = require('lodash/pick');

// Deliberate: node and 3rd party modules before global-tunnel
var EventEmitter = require('events').EventEmitter;
var net = require('net');
var tls = require('tls');
var http = require('http');
var globalHttpAgent = http.globalAgent;
var https = require('https');
var globalHttpsAgent = https.globalAgent;
var request = require('request');

// Deliberate: load after all 3rd party modules
var globalTunnel = require('../index');

function newFakeAgent() {
  var fakeAgent = {
    addRequest: sinon.stub()
  };
  return fakeAgent;
}

// This function replaces 'host' by 'hostname' in the options for http.request()
// background: http.request() allows to use either 'host' or 'hostname' to be used,
// both needs to be tested
function replaceHostByHostname(useHostname, options) {
  if (useHostname) {
    options.hostname = options.host;
    delete options.host;
  }
  return options;
}

var origEnv;
function saveEnv() {
  origEnv = process.env.http_proxy;
  delete process.env.http_proxy;
}
function restoreEnv() {
  if (origEnv !== undefined) {
    process.env.http_proxy = origEnv; // eslint-disable-line camelcase
  }
}

describe('global-proxy', function() {
  // Save and restore http_proxy environment variable (yes, it's lower-case by
  // convention).
  before(saveEnv);
  after(restoreEnv);

  // Sinon setup & teardown
  var sandbox;
  var origHttpCreateConnection;

  before(function() {
    sandbox = sinon.createSandbox();

    sandbox.stub(globalHttpAgent, 'addRequest');
    sandbox.stub(globalHttpsAgent, 'addRequest');

    assert.equal(http.Agent.prototype.addRequest, https.Agent.prototype.addRequest);
    sandbox.spy(http.Agent.prototype, 'addRequest');

    sandbox.stub(net, 'createConnection').callsFake(function() {
      return new EventEmitter();
    });
    sandbox.stub(tls, 'connect').callsFake(function() {
      return new EventEmitter();
    });

    // This is needed as at some point Node HTTP aggent implementation started
    // plucking the createConnection method from the `net` module
    // instead of doing `net.createConnection`
    origHttpCreateConnection = http.Agent.prototype.createConnection;
    http.Agent.prototype.createConnection = net.createConnection;
  });

  afterEach(function() {
    sandbox.resetHistory();
  });

  after(function() {
    sandbox.restore();
    http.Agent.prototype.createConnection = origHttpCreateConnection;
  });

  describe('invalid configs', function() {
    it('requires a host', function() {
      var conf = { host: null, port: 1234 };
      assert.throws(function() {
        globalTunnel.initialize(conf);
      }, 'upstream proxy host is required');
      globalTunnel.end();
    });

    it('requires a port', function() {
      var conf = { host: '10.2.3.4', port: 0 };
      assert.throws(function() {
        globalTunnel.initialize(conf);
      }, 'upstream proxy port is required');
      globalTunnel.end();
    });

    it('clamps tunnel types', function() {
      var conf = { host: '10.2.3.4', port: 1234, connect: 'INVALID' };
      assert.throws(function() {
        globalTunnel.initialize(conf);
      }, 'valid connect options are "neither", "https", or "both"');
      globalTunnel.end();
    });
  });

  describe('exposed config', function() {
    afterEach(function() {
      globalTunnel.end();
    });

    it('has the same params as the passed config', function() {
      var conf = {
        host: 'proxy.com',
        port: 1234,
        proxyAuth: 'user:pwd',
        protocol: 'https:'
      };
      globalTunnel.initialize(conf);
      assert.deepEqual(
        conf,
        pick(globalTunnel.proxyConfig, ['host', 'port', 'proxyAuth', 'protocol'])
      );
    });

    it('has the expected defaults', function() {
      var conf = { host: 'proxy.com', port: 1234, proxyAuth: 'user:pwd' };
      globalTunnel.initialize(conf);
      assert.equal(globalTunnel.proxyConfig.protocol, 'http:');
    });
  });

  describe('stringified config', function() {
    afterEach(function() {
      globalTunnel.end();
    });

    it('has the same params as the passed config', function() {
      var conf = {
        host: 'proxy.com',
        port: 1234,
        proxyAuth: 'user:pwd',
        protocol: 'https'
      };
      globalTunnel.initialize(conf);
      assert.equal(globalTunnel.proxyUrl, 'https://user:pwd@proxy.com:1234');
    });

    it('encodes url', function() {
      var conf = {
        host: 'proxy.com',
        port: 1234,
        proxyAuth: 'user:4P@S$W0_r-D',
        protocol: 'https'
      };
      globalTunnel.initialize(conf);
      assert.equal(globalTunnel.proxyUrl, 'https://user:4P%40S%24W0_r-D@proxy.com:1234');
    });
  });

  function proxyEnabledTests(testParams) {
    function connected(innerProto) {
      var innerSecure = innerProto === 'https:';

      var called;
      if (testParams.isHttpsProxy) {
        called = tls.connect;
        sinon.assert.notCalled(net.createConnection);
      } else {
        called = net.createConnection;
        sinon.assert.notCalled(tls.connect);
      }

      sinon.assert.calledOnce(called);
      if (typeof called.getCall(0).args[0] === 'object') {
        sinon.assert.calledWith(called, sinon.match.has('port', testParams.port));
        sinon.assert.calledWith(called, sinon.match.has('host', '10.2.3.4'));
      } else {
        sinon.assert.calledWith(called, testParams.port, '10.2.3.4');
      }

      var isCONNECT =
        testParams.connect === 'both' || (innerSecure && testParams.connect === 'https');
      if (isCONNECT) {
        var expectConnect = 'example.dev:' + (innerSecure ? 443 : 80);
        var whichAgent = innerSecure ? https.globalAgent : http.globalAgent;

        sinon.assert.calledOnce(whichAgent.request);
        sinon.assert.calledWith(whichAgent.request, sinon.match.has('method', 'CONNECT'));
        sinon.assert.calledWith(
          whichAgent.request,
          sinon.match.has('path', expectConnect)
        );
      } else {
        sinon.assert.calledOnce(http.Agent.prototype.addRequest);
        var req = http.Agent.prototype.addRequest.getCall(0).args[0];

        var method = req.method;
        assert.equal(method, 'GET');

        var path = req.path;
        if (innerSecure) {
          assert.match(path, new RegExp('^https://example\\.dev:443/'));
        } else {
          assert.match(path, new RegExp('^http://example\\.dev:80/'));
        }
      }
    }

    var localSandbox;
    beforeEach(function() {
      localSandbox = sinon.createSandbox();
      if (testParams.connect === 'both') {
        localSandbox.spy(http.globalAgent, 'request');
      }
      if (testParams.connect !== 'neither') {
        localSandbox.spy(https.globalAgent, 'request');
      }
    });
    afterEach(function() {
      localSandbox.restore();
    });

    it('(got proxying set up)', function() {
      assert.isTrue(globalTunnel.isProxying);
    });

    describe('with the request library', function() {
      it('will proxy http requests', function(done) {
        assert.isTrue(globalTunnel.isProxying);
        var dummyCb = sinon.stub();
        request.get('http://example.dev/', dummyCb);
        setImmediate(function() {
          connected('http:');
          sinon.assert.notCalled(globalHttpAgent.addRequest);
          sinon.assert.notCalled(globalHttpsAgent.addRequest);
          done();
        });
      });

      it('will proxy https requests', function(done) {
        assert.isTrue(globalTunnel.isProxying);
        var dummyCb = sinon.stub();
        request.get('https://example.dev/', dummyCb);
        setImmediate(function() {
          connected('https:');
          sinon.assert.notCalled(globalHttpAgent.addRequest);
          sinon.assert.notCalled(globalHttpsAgent.addRequest);
          done();
        });
      });
    });

    describe('using raw request interface', function() {
      function rawRequest(useHostname) {
        var req = http.request(
          replaceHostByHostname(useHostname, {
            method: 'GET',
            path: '/raw-http',
            host: 'example.dev'
          }),
          function() {}
        );
        req.end();

        connected('http:');
        sinon.assert.notCalled(globalHttpAgent.addRequest);
        sinon.assert.notCalled(globalHttpsAgent.addRequest);
      }
      it('will proxy http requests (`host`)', function() {
        rawRequest(false);
      });
      it('will proxy http requests (`hostname`)', function() {
        rawRequest(true);
      });

      it('will proxy https requests', function() {
        var req = https.request(
          replaceHostByHostname(false, {
            method: 'GET',
            path: '/raw-https',
            host: 'example.dev'
          }),
          function() {}
        );
        req.end();

        connected('https:');
        sinon.assert.notCalled(globalHttpAgent.addRequest);
        sinon.assert.notCalled(globalHttpsAgent.addRequest);
      });

      it('request respects explicit agent param', function() {
        var agent = newFakeAgent();
        var req = http.request(
          replaceHostByHostname(false, {
            method: 'GET',
            path: '/raw-http-w-agent',
            host: 'example.dev',
            agent: agent
          }),
          function() {}
        );
        req.end();

        sinon.assert.notCalled(globalHttpAgent.addRequest);
        sinon.assert.notCalled(globalHttpsAgent.addRequest);
        sinon.assert.notCalled(net.createConnection);
        sinon.assert.notCalled(tls.connect);
        sinon.assert.calledOnce(agent.addRequest);
      });

      describe('request with `null` agent and defined `createConnection`', function() {
        before(function() {
          sinon.stub(http.ClientRequest.prototype, 'onSocket');
        });
        after(function() {
          http.ClientRequest.prototype.onSocket.restore();
        });

        function noAgent(useHostname) {
          var createConnection = sinon.stub();
          var req = http.request(
            replaceHostByHostname(useHostname, {
              method: 'GET',
              path: '/no-agent',
              host: 'example.dev',
              agent: null,
              createConnection: createConnection
            }),
            function() {} // eslint-disable-line max-nested-callbacks
          );
          req.end();

          sinon.assert.notCalled(globalHttpAgent.addRequest);
          sinon.assert.notCalled(globalHttpsAgent.addRequest);
          sinon.assert.calledOnce(createConnection);
        }
        it('uses no agent (`host`)', function() {
          noAgent(false);
        });
        it('uses no agent (`hostname`)', function() {
          noAgent(true);
        });
      });
    });
  }

  function enabledBlock(conf, testParams) {
    before(function() {
      globalTunnel.initialize(conf);
    });
    after(function() {
      globalTunnel.end();
    });

    testParams = assign(
      {
        port: conf && conf.port,
        isHttpsProxy: conf && conf.protocol === 'https:',
        connect: (conf && conf.connect) || 'https'
      },
      testParams
    );

    proxyEnabledTests(testParams);
  }

  describe('with http proxy in intercept mode', function() {
    enabledBlock({
      connect: 'neither',
      protocol: 'http:',
      host: '10.2.3.4',
      port: 3333
    });
  });

  describe('with https proxy in intercept mode', function() {
    enabledBlock({
      connect: 'neither',
      protocol: 'https:',
      host: '10.2.3.4',
      port: 3334
    });
  });

  describe('with http proxy in CONNECT mode', function() {
    enabledBlock({
      connect: 'both',
      protocol: 'http:',
      host: '10.2.3.4',
      port: 3335
    });
  });

  describe('with https proxy in CONNECT mode', function() {
    enabledBlock({
      connect: 'both',
      protocol: 'https:',
      host: '10.2.3.4',
      port: 3336
    });
  });

  describe('with http proxy in mixed mode', function() {
    enabledBlock({
      protocol: 'http:',
      host: '10.2.3.4',
      port: 3337
    });
  });

  describe('with https proxy in mixed mode', function() {
    enabledBlock({
      protocol: 'https:',
      host: '10.2.3.4',
      port: 3338
    });
  });

  describe('using env var', function() {
    after(function() {
      delete process.env.http_proxy;
      assert.isUndefined(process.env.http_proxy);
    });

    describe('for http', function() {
      before(function() {
        process.env.http_proxy = 'http://10.2.3.4:1234'; // eslint-disable-line camelcase
      });
      enabledBlock(null, { isHttpsProxy: false, connect: 'https', port: 1234 });
    });

    describe('for https', function() {
      before(function() {
        process.env.http_proxy = 'https://10.2.3.4:1235'; // eslint-disable-line camelcase
      });
      enabledBlock(null, { isHttpsProxy: true, connect: 'https', port: 1235 });
    });
  });

  describe('using npm config', function() {
    var expectedProxy = { isHttpsProxy: false, connect: 'https', port: 1234 };
    var npmConfig = { get: function() {} };
    var npmConfigStub = sinon.stub(npmConfig, 'get');

    function configNpm(key, value) {
      return function() {
        global.__GLOBAL_TUNNEL_DEPENDENCY_NPMCONF__ = function() {
          return npmConfig;
        };

        npmConfigStub.withArgs(key).returns(value || 'http://10.2.3.4:1234');
      };
    }

    after(function() {
      global.__GLOBAL_TUNNEL_DEPENDENCY_NPMCONF__ = undefined;
    });

    describe('https-proxy', function() {
      before(configNpm('https-proxy'));
      enabledBlock(null, expectedProxy);
    });

    describe('http-proxy', function() {
      before(configNpm('http-proxy'));
      enabledBlock(null, expectedProxy);
    });

    describe('proxy', function() {
      before(configNpm('proxy'));
      enabledBlock(null, expectedProxy);
    });
    describe('order', function() {
      before(function() {
        configNpm('proxy')();
        configNpm('https-proxy', 'http://10.2.3.4:12345')();
        configNpm('http-proxy')();
      });

      enabledBlock(null, { isHttpsProxy: false, connect: 'https', port: 12345 });
    });

    describe('also using env var', function() {
      before(function() {
        configNpm('proxy')();
        process.env.http_proxy = 'http://10.2.3.4:1234'; // eslint-disable-line camelcase
      });

      after(function() {
        delete process.env.http_proxy;
      });

      enabledBlock(null, expectedProxy);
    });
  });

  // Deliberately after the block above
  describe('with proxy disabled', function() {
    it('claims to be disabled', function() {
      assert.isFalse(globalTunnel.isProxying);
    });

    it('will NOT proxy http requests', function(done) {
      var dummyCb = sinon.stub();
      request.get('http://example.dev/', dummyCb);
      setImmediate(function() {
        sinon.assert.calledOnce(globalHttpAgent.addRequest);
        sinon.assert.notCalled(globalHttpsAgent.addRequest);
        done();
      });
    });

    it('will NOT proxy https requests', function(done) {
      var dummyCb = sinon.stub();
      request.get('https://example.dev/', dummyCb);
      setImmediate(function() {
        sinon.assert.notCalled(globalHttpAgent.addRequest);
        sinon.assert.calledOnce(globalHttpsAgent.addRequest);
        done();
      });
    });
  });
});