const {  setup, HTTP_PORT, HTTPS_PORT, INACTIVE_PORT, CORRUPT_PORT } = require('./helpers');
const assert  = require('assert');
const File    = require('fs');
const HTTP    = require('http');
const HTTPS   = require('https');
const Async   = require('async');
const Request = require('request');
const Replay  = require('../src');


// Test replaying results from fixtures in spec/fixtures.
describe('Replay', function() {


  // Send responses to non-existent server on inactive port, expect replayed responses from fixtures.
  describe('matching URL', function() {

    before(function() {
      Replay.mode = 'replay';
    });

    describe('listeners', function() {
      let response;

      before(function(done) {
        HTTP
          .get(`http://example.com:${INACTIVE_PORT}/weather?c=94606`, function(_) {
            response = _;
            done();
          })
          .on('error', done);
      });

      it('should return HTTP version', function() {
        assert.equal(response.httpVersion, '1.1');
      });
      it('should return status code', function() {
        assert.equal(response.statusCode, 200);
      });
      it('should return response headers', function() {
        assert.deepEqual(response.headers, {
          'content-type': 'text/html',
          'date':         'Tue, 29 Nov 2011 03:12:15 GMT'
        });
      });
      it('should return response trailers', function() {
        assert.deepEqual(response.trailers, { });
      });
    });

    describe('Old http status line format', function() {
      let response;

      before(function(done) {
        HTTP
          .get(`http://example.com:${INACTIVE_PORT}/weather?c=94606&statusLineFormat=old`, function(_) {
            response = _;
            done();
          })
          .on('error', done);
      });

      it('should return HTTP version', function() {
        assert.equal(response.httpVersion, '1.1');
      });
      it('should return status code', function() {
        assert.equal(response.statusCode, 200);
      });
      it('should return response headers', function() {
        assert.deepEqual(response.headers, {
          'content-type': 'text/html',
          'date':         'Tue, 29 Nov 2011 03:12:15 GMT'
        });
      });
      it('should return response trailers', function() {
        assert.deepEqual(response.trailers, { });
      });
    });


    describe('callback', function() {
      let response;

      before(function(done) {
        HTTP.get({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          path:     '/weather?c=94606'
        }, function(_) {
          response = _;
          response.body = '';
          response.on('data', function(chunk) {
            response.body += chunk;
          });
          response.on('end', done);
        })
        .on('error', done);
      });

      it('should return HTTP version', function() {
        assert.equal(response.httpVersion, '1.1');
      });
      it('should return status code', function() {
        assert.equal(response.statusCode, 200);
      });
      it('should return response headers', function() {
        assert.deepEqual(response.headers, {
          'content-type': 'text/html',
          'date':         'Tue, 29 Nov 2011 03:12:15 GMT'
        });
      });
      it('should return response trailers', function() {
        assert.deepEqual(response.trailers, { });
      });
    });
  });


  describe('matching on query strings', function() {
    let response1;
    let response2;

    before(function(done) {
      HTTP.get({ hostname: 'example.com', port: INACTIVE_PORT, path: '/query?param=1' })
        .on('response', function(_) {
          response1 = _;
          response1.body = '';
          response1.on('data', function(chunk) {
            response1.body += chunk;
          });
          response1.on('end', done);
        })
        .on('error', done);
    });

    before(function(done) {
      HTTP.get({ hostname: 'example.com', port: INACTIVE_PORT, path: '/query?param=2' })
        .on('response', function(_) {
          response2 = _;
          response2.body = '';
          response2.on('data', function(chunk) {
            response2.body += chunk;
          });
          response2.on('end', done);
        })
        .on('error', done);
    });

    it('should select the correct fixture', function() {
      // HTTP body contains tailing line feeds, use trim to get rid of them
      assert.equal(response1.body.trim(), '1');
      assert.equal(response2.body.trim(), '2');
    });
  });


  describe('matching an https url', function() {
    let response;

    before(function() {
      Replay.mode = 'replay';
    });

    before(function(done) {
      HTTPS.get({ hostname: 'example.com', port: HTTPS_PORT, path: '/minimal' })
        .on('response', function(_) {
          response = _;
          done();
        })
        .on('error', done);
    });

    it('should return HTTP version', function() {
      assert.equal(response.httpVersion, '1.1');
    });
    it('should return status code', function() {
      assert.equal(response.statusCode, 200);
    });
  });


  describe('not matching hostname, using Request', function() {
    let error;

    before(function() {
      Replay.mode = 'replay';
    });

    before(function(done) {
      Request.get('https://www.nytimes.com', function(_, res) {
        error = _;
        done();
      });
    });

    it('should callback with error', function() {
      assert(error instanceof Error);
      assert.equal(error.code, 'ECONNREFUSED');
    });
  });


  describe('matching a regexp', function() {
    let response;

    before(function() {
      Replay.mode = 'replay';
    });

    before(function(done) {
      HTTP.get({ hostname: 'example.com', port: INACTIVE_PORT, method: 'get', path: '/regexp' })
        .on('response', function(_) {
          response = _;
          response.body = '';
          response.on('data', function(chunk) {
            response.body += chunk;
          });
          response.on('end', done);
        })
        .on('error', done);
    });

    it('should match the right fixture', function() {
      assert.equal(response.body.trim(), 'regexp');
    });
  });


  describe('matching a regexp url with flags', function() {
    let response;

    before(function() {
      Replay.mode = 'replay';
    });

    before(function(done) {
      HTTP.get({ hostname: 'example.com', port: INACTIVE_PORT, method: 'get', path: '/aregexp2' })
        .on('response', function(_) {
          response = _;
          response.body = '';
          response.on('data', function(chunk) {
            response.body += chunk;
          });
          response.on('end', done);
        })
        .on('error', done);
    });

    it('should match a fixture', function() {
      assert.equal(response.body.trim(), 'Aregexp2');
    });
  });


  describe('matching when changing fixtures dir', function() {
    before(function() {
      Replay.mode = 'replay';
    });

    describe('original catalog', function() {
      let response;

      before(function(done) {
        HTTP.get({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          path:     '/weather?c=94606'
        }, function(_) {
          response = _;
          response.body = '';
          response.on('data', function(chunk) {
            response.body += chunk;
          });
          response.on('end', done);
        })
        .on('error', done);
      });

      it('should match to response in original catalog', function() {
        assert.equal(response.headers.date, 'Tue, 29 Nov 2011 03:12:15 GMT');
        assert.equal(response.body, 'Nice and warm\n');
      });

    });

    describe('alternative catalog', function() {
      let response;

      before(function() {
        Replay.fixtures = `${__dirname}/fixtures/other-fixtures-dir`;
      });

      before(function(done) {
        HTTP.get({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          path:     '/weather?c=94606'
        }, function(_) {
          response = _;
          response.body = '';
          response.on('data', function(chunk) {
            response.body += chunk;
          });
          response.on('end', done);
        })
        .on('error', done);
      });

      it('should match to response in original catalog', function() {
        assert.equal(response.headers.date, 'Tue, 30 Nov 2011 03:12:15 GMT');
        assert.equal(response.body, 'Sweet and cold\n');
      });

      after(function() {
        Replay.fixtures = `${__dirname}/fixtures`;
      });
    });

  });


  describe('recording query parameters', function() {
    const fixturesDir = `${__dirname}/fixtures/127.0.0.1-${HTTP_PORT}`;

    before(setup);

    before(function() {
      Replay.mode = 'record';
      Replay.reset('127.0.0.1');
    });

    it('should create a fixture per unique URL path', function(done) {
      const requests = [
        { name: 'Lorem', extra: 'Ipsum'},
        { name: 'Dolor', extra: 'Sit'}
      ].map(function(query) {
        return function(callback) {
          Request.get({
            url:    `http://127.0.0.1:${HTTP_PORT}/query`,
            qs:     query,
            json:   true
          }, function(error, response, body) {
            if (error)
              callback(error);
            else
              try {
                assert.deepEqual(body, query);
                callback(null, query);
              } catch (error) {
                callback(error);
              }
          });
        };
      });

      Async.series(requests, function(error) {
        if (error)
          done(error);
        else {
          // fixtures should be written now
          Replay.mode = 'replay';
          Async.series(requests, done);
        }
      });
    });

    after(function() {
      for(let file of File.readdirSync(fixturesDir))
        File.unlinkSync(`${fixturesDir}/${file}`);
      File.rmdirSync(fixturesDir);
    });

  });


  describe('recording gzipped replay', function() {
    const fixturesDir = `${__dirname}/fixtures/127.0.0.1-${HTTP_PORT}`;

    before(setup);

    before(function() {
      Replay.mode = 'record';
      Replay.reset('127.0.0.1');
    });

    it('should create unzipped fixture for gzipped reply', function(done) {
      const query = { name: 'Amet', extra: 'consectetur'};
      const request = function(callback) {
        Request.get({
          url:    `http://127.0.0.1:${HTTP_PORT}/query`,
          qs:     query,
          headers: {
            'accept-encoding': 'gzip'
          },
          json:   true
        }, function(error, response, body) {
          if (error)
            callback(error);
          else
            try {
              assert.deepEqual(body, query);
              callback(null, query);
            } catch (error) {
              callback(error);
            }
        });
      };

      request(function(error) {
        if (error)
          done(error);
        else {
          // fixtures should be written now
          Replay.mode = 'replay';
          request(done);
        }
      });
    });

    after(function() {
      for(let file of File.readdirSync(fixturesDir))
        File.unlinkSync(`${fixturesDir}/${file}`);
      File.rmdirSync(fixturesDir);
    });

  });


  describe('recording multiple of the same header', function() {
    const fixturesDir = `${__dirname}/fixtures/127.0.0.1-${HTTP_PORT}`;

    before(setup);

    before(function() {
      Replay.mode = 'record';
      Replay.reset('127.0.0.1');
    });

    before(function(done) {
      HTTP.get({ hostname: '127.0.0.1', port: HTTP_PORT, path: '/set-cookie' })
        .on('response', function(response) {
          response.on('end', done);
        })
        .on('error', done);
    });

    it('should create a fixture with multiple set-cookie headers', function() {
      let setCookieCount = 0;
      const files   = File.readdirSync(fixturesDir);
      const fixture = File.readFileSync(`${fixturesDir}/${files[0]}`, 'utf8');
      for (let line of fixture.split('\n'))
        if (/set-cookie: c\d=v\d/.test(line))
          setCookieCount++;
      assert.equal(setCookieCount, 2);
    });


    describe('replaying multiple headers', function() {
      let headers;

      before(function(done) {
        Request.get(`http://127.0.0.1:${HTTP_PORT}/set-cookie`, function(error, resp) {
          headers = resp.headers;
          done(error);
        });
      });

      it('should have both set-cookie headers', function() {
        assert.equal(headers['set-cookie'][0], 'c1=v1; Path=/');
        assert.equal(headers['set-cookie'][1], 'c2=v2; Path=/');
      });
    });


    after(function() {
      for (let file of File.readdirSync(fixturesDir))
        File.unlinkSync(`${fixturesDir}/${file}`);
      File.rmdirSync(fixturesDir);
    });
  });


  describe('recording POST data', function() {

    const fixturesDir = `${__dirname}/fixtures/127.0.0.1-${HTTP_PORT}`;

    function setupPostRequest(done) {
      const request = HTTP.request({
        hostname: '127.0.0.1',
        port:     HTTP_PORT,
        method:   'post',
        path:     '/post-data'
      }, function(response) {
        response.on('end', done);
      });
      request.write('request data');
      request.end();
    }

    function hasSavedPostRequestData() {
      if (File.existsSync(fixturesDir)) {
        let hasData   = false;
        const files   = File.readdirSync(fixturesDir);
        const fixture = File.readFileSync(`${fixturesDir}/${files[0]}`, 'utf8');
        for (let line of fixture.split('\n'))
          if (line === 'body: request data')
            hasData = true;
        return hasData;
      }
    }

    before(setup);

    before(function() {
      Replay.mode = 'record';
      Replay.reset('127.0.0.1');
    });

    context('without record response control', function() {
      before(setupPostRequest);

      it('should save POST request data', function() {
        assert(hasSavedPostRequestData());
      });
    });

    describe('with record response control', function() {
      context('that indicates response should be recorded', function() {
        before(function() {
          Replay.recordResponseControl = {
            ['127.0.0.1:' + HTTP_PORT] : function() {
              return true;
            }
          };
        });

        before(setupPostRequest);

        it('should save POST request data', function() {
          assert(hasSavedPostRequestData());
        });
      });
      context('that indicates response should not be recorded', function() {
        before(function() {
          Replay.recordResponseControl = {
            ['127.0.0.1:' + HTTP_PORT] : function() {
              return false;
            }
          };
        });

        before(setupPostRequest);

        it('should not save POST request data', function() {
          assert(!hasSavedPostRequestData());
        });
      });

      after(function() {
        Replay.recordResponseControl = null;
      });
    });



    afterEach(function() {
      if (File.existsSync(fixturesDir)) {
        for (let file of File.readdirSync(fixturesDir))
          File.unlinkSync(`${fixturesDir}/${file}`);
        File.rmdirSync(fixturesDir);
      }
    });
  });


  describe('recording multi-line POST data', function() {
    const fixturesDir = `${__dirname}/fixtures/127.0.0.1-${HTTP_PORT}`;

    before(setup);

    before(function() {
      Replay.mode = 'record';
      Replay.reset('127.0.0.1');
    });

    before(function(done) {
      const request = HTTP.request({
        hostname: '127.0.0.1',
        port:     HTTP_PORT,
        method:   'post',
        path: '/post-data'
      }, function(response) {
        response.on('end', done);
      });
      request.write('line1\nline2\nline3');
      request.end();
    });

    it('should save POST request data', function() {
      const files   = File.readdirSync(fixturesDir);
      const fixture = File.readFileSync(`${fixturesDir}/${files[0]}`, 'utf8');
      assert.equal(fixture.split('\n')[1], 'body: line1\\nline2\\nline3');
    });

    after(function() {
      for (let file of File.readdirSync(fixturesDir))
        File.unlinkSync(`${fixturesDir}/${file}`);
      File.rmdirSync(fixturesDir);
    });
  });


  describe('replaying with POST body', function() {
    before(function() {
      Replay.mode = 'replay';
    });

    describe('matching', function() {
      let response;

      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          method:   'post',
          path:     '/post-body'
        }, function(_) {
          response = _;
          response.on('end', done);
        })
        .on('error', done);
        request.write('request body');
        request.end();
      });

      it('should return status code', function() {
        assert.equal(response.statusCode, 200);
      });
    });

  });


  describe('replaying with multi-line POST body', function() {
    let response;

    before(function() {
      Replay.mode = 'replay';
    });

    describe('matching', function() {
      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          method:   'post',
          path:     '/post-body-multi'
        }, function(_) {
          response = _;
          response.on('end', done);
        })
        .on('error', done);
        request.write('line1\nline2\nline3');
        request.end();
      });

      it('should return status code', function() {
        assert.equal(response.statusCode, 200);
      });
    });
  });


  describe('replaying with POST body regular expression', function() {
    before(function() {
      Replay.mode = 'replay';
    });

    describe('matching', function() {
      let response;

      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          method:   'post',
          path:     '/post-body-regexp'
        }, function(_) {
          response = _;
          response.on('end', done);
        })
        .on('error', done);
        request.write('request body 17');
        request.end();
      });

      it('should return status code', function() {
        assert.equal(response.statusCode, 200);
      });
    });

    describe('not matching', function() {
      let error;

      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          method:   'post',
          path:     '/post-body-regexp'
        }, function(_) {
          response = _;
          response.on('end', done);
        })
        .on('error', function(_) {
          error = _;
          done();
        });
        request.write('request body ABC');
        request.end();
      });

      it('should callback with error', function() {
        assert(error instanceof Error);
        assert.equal(error.code, 'ECONNREFUSED');
      });
    });

  });


  describe('only specified headers', function() {
    const fixturesDir = `${__dirname}/fixtures/127.0.0.1-${HTTP_PORT}`;

    before(setup);

    before(function() {
      Replay.mode = 'record';
      Replay.reset('127.0.0.1');
      // Drop the /accept/ header
      Replay.headers = Replay.headers.filter(header => !header.test('accept'));
    });

    before(function(done) {
      HTTP.get({ hostname: '127.0.0.1', port: HTTP_PORT, path: '/', headers: { accept: 'application/json' } })
        .on('response', function(response) {
          response.on('end', done);
        })
        .on('error', done);
    });

    it('should not store the accept header', function() {
      const files   = File.readdirSync(fixturesDir);
      const fixture = File.readFileSync(`${fixturesDir}/${files[0]}`, 'utf8');
      assert(!/accept/.test(fixture));
    });

    after(function() {
      for (let file of File.readdirSync(fixturesDir))
        File.unlinkSync(`${fixturesDir}/${file}`);
      File.rmdirSync(fixturesDir);
    });
  });



  // Send responses to non-existent server on inactive port. No matching fixture for that path, expect a 404.
  describe('undefined path', function() {
    let error;

    before(function() {
      Replay.mode = 'replay';
    });

    before(function(done) {
      HTTP.get({ hostname: 'example.com', port: INACTIVE_PORT, path: '/weather?c=14003' })
        .on('response', done)
        .on('error', function(_) {
          error = _;
          done();
        });
    });

    it('should callback with error', function() {
      assert(error instanceof Error);
      assert.equal(error.code, 'ECONNREFUSED');
    });
  });


  // Send responses to non-existent server on inactive port. No matching fixture for that host, expect refused connection.
  describe('undefined host', function() {
    let error;

    before(function() {
      Replay.mode = 'default';
    });

    before(function(done) {
      HTTP.get({ hostname: 'no-such', port: INACTIVE_PORT })
        .on('response', done)
        .on('error', function(_) {
          error = _;
          done();
        });
    });

    it('should callback with error', function() {
      assert(error instanceof Error);
      assert.equal(error.code, 'ECONNREFUSED');
    });
  });


  // Mapping specifies a header, make sure we only match requests that have that header value.
  describe('header', function() {
    before(function() {
      Replay.mode = 'replay';
    });

    describe('matching', function() {
      let statusCode;

      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          path: '/weather.json'
        });
        request.setHeader('Accept', 'application/json');
        request.on('response', function(response) {
          statusCode = response.statusCode;
          response.on('end', done);
        });
        request.on('error', done);
        request.end();
      });

      it('should return status code', function() {
        assert.equal(statusCode, 200);
      });
    });

    describe('no match', function() {
      let error;

      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          path:     '/weather.json'
        });
        request.setHeader('Accept', 'text/html');
        request.on('response', function(response) {
          response.on('end', done);
        });
        request.on('error', function(_) {
          error = _;
          done();
        });
        request.end();
      });

      it('should fail to connnect', function() {
        assert(error instanceof Error);
      });
    });

  });


  describe('method', function() {
    before(function() {
      Replay.mode = 'replay';
    });

    describe('matching', function() {
      let statusCode, headers;

      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          method:   'post',
          path:     '/posts'
        });
        request.setHeader('Accept', 'application/json');
        request.on('response', function(response) {
          statusCode = response.statusCode;
          headers    = response.headers;
          response.on('end', done);
        });
        request.on('error', done);
        request.end();
      });

      it('should return status code', function() {
        assert.equal(statusCode, 201);
      });
      it('should return headers', function() {
        assert.equal(headers.location, '/posts/1');
      });
    });

    describe('no match', function() {
      let error;

      before(function(done) {
        const request = HTTP.request({
          hostname: 'example.com',
          port:     INACTIVE_PORT,
          method:   'put',
          path:     '/posts'
        });
        request.setHeader('Accept', 'application/json');
        request.on('response', function(response) {
          response.on('end', done);
        });
        request.on('error', function(_) {
          error = _;
          done();
        });
        request.end();
      });

      it('should fail to connnect', function() {
        assert(error instanceof Error);
      });
    });

  });


  describe('corrupt replay file', function() {
    let error;

    before(function() {
      Replay.mode = 'default';
    });

    before(function(done) {
      HTTP.get({ hostname: 'example.com', port: CORRUPT_PORT, path: '/minimal' })
        .on('response', done)
        .on('error', function(_) {
          error = _;
          done();
        });
    });

    it('should callback with error', function() {
      assert(error instanceof Error);
      assert.equal(error.code, 'CORRUPT FIXTURE');
    });
  });


  describe('minimal response', function() {
    before(function() {
      Replay.mode = 'replay';
    });

    describe('listeners', function() {
      let response;

      before(function(done) {
        HTTP.get({ hostname: 'example.com', port: INACTIVE_PORT, path: '/minimal' })
          .on('response', function(_) {
            response = _;
            response.body = '';
            response.on('data', function(chunk) {
              response.body = response.body + chunk;
            });
            response.on('end', done);
            response.on('error', done);
          })
          .on('error', done);
      });

      it('should return HTTP version', function() {
        assert.equal(response.httpVersion, '1.1');
      });
      it('should return status code', function() {
        assert.equal(response.statusCode, 200);
      });
      it('should return no response headers', function() {
        assert.deepEqual(response.headers, { });
      });
      it('should return no response trailers', function() {
        assert.deepEqual(response.trailers, { });
      });
      it('should return no response body', function() {
        assert.equal(response.body, '');
      });
    });

  });

  describe('monkeypatching', function() {
    function reload(path) {
      delete require.cache[require.resolve(path)];
      require(path);
    }


    it('shouldn\'t patch HTTPS.request twice', function() {
      const patchedRequest = HTTPS.request;
      reload('../src/patch_http_request');
      assert.equal(patchedRequest, HTTPS.request);
      assert.equal(true, HTTPS['__patched_by_replay__']);
    })

    it('shouldn\'t patch HTTP.request twice', function() {
      const patchedRequest = HTTP.request;
      reload('../src/patch_http_request');
      assert.equal(patchedRequest, HTTP.request);
      assert.equal(true, HTTP['__patched_by_replay__']);
    })

    it('shouldn\'t patch HTTPS.get twice', function() {
      const patchedGet = HTTPS.get;
      reload('../src/patch_http_request');

      assert.equal(patchedGet, HTTPS.get);
      assert.equal(true, HTTPS['__patched_by_replay__']);
    })

    it('shouldn\'t patch HTTP.get twice', function() {
      const patchedGet = HTTP.get;
      reload('../src/patch_http_request');

      assert.equal(patchedGet, HTTP.get);
      assert.equal(true, HTTP['__patched_by_replay__']);
    });

    it('shouldn\'t patch DNS.lookup twice', function() {
      const DNS = require('dns');
      const patchedLookup = DNS.lookup;
      reload('../src/patch_dns_lookup');

      assert.equal(patchedLookup, DNS.lookup);
      assert.equal(true, DNS['__patched_by_replay__']);
    });
  })

});

