/* eslint-env mocha */

var Transform = require('stream').Transform;
var fs = require('fs');
var path = require('path');

var chai = require('chai');
var gutil = require('gulp-util');
var mock = require('mock-fs');

var newer = require('./index.js');

chai.config.includeStack = true;

var File = gutil.File;
var assert = chai.assert;

/**
 * Test utility function.  Create File instances for each of the provided paths
 * and write to the provided stream.  Call stream.end() when done.
 * @param {stream.Transform} stream Transform stream.
 * @param {Array.<string>} paths Array of file paths.
 */
function write(stream, paths) {
  paths.forEach(function(filePath) {
    stream.write(new File({
      contents: fs.readFileSync(filePath),
      path: path.resolve(filePath),
      stat: fs.statSync(filePath)
    }));
  });
  stream.end();
}

describe('newer()', function() {

  it('creates a transform stream', function() {
    var stream = newer('foo');
    assert.instanceOf(stream, Transform);
  });

  it('requires a string dest or an object with the dest property', function() {

    assert.throws(function() {
      newer();
    });

    assert.throws(function() {
      newer(123);
    });

    assert.throws(function() {
      newer({});
    });
  });

  describe('config.ext', function() {

    it('must be a string', function() {

      assert.throws(function() {
        newer({dest: 'foo', ext: 1});
      });

      assert.throws(function() {
        newer({dest: 'foo', ext: {}});
      });
    });
  });

  describe('config.map', function() {

    it('must be a function', function() {
      assert.throws(function() {
        newer({dest: 'foo', map: 1});
      });

      assert.throws(function() {
        newer({dest: 'foo', map: 'bar'});
      });
    });

    it('makes the dest config optional', function() {
      assert.doesNotThrow(function() {
        newer({map: function() {}});
      });
    });

  });

  describe('config.extra', function() {

    beforeEach(function() {
      mock({
        main: mock.file({
          content: 'main content',
          mtime: new Date(1)
        }),
        imported: mock.file({
          content: '2: other content, used by main',
          mtime: new Date(3)
        }),
        collected: mock.file({
          content: 'main content\n1: other content, used by main',
          mtime: new Date(2)
        })
      });
    });
    afterEach(mock.restore);

    it('must be a string or an array', function() {
      assert.throws(function() {
        newer({dest: 'foo', extra: 1});
      });

      assert.throws(function() {
        newer({dest: 'foo', extra: function() {}});
      });

      assert.doesNotThrow(function() {
        newer({dest: 'foo', extra: 'extra1'});
      });

      assert.doesNotThrow(function() {
        newer({dest: 'foo', extra: ['extra1', 'extra2']});
      });
    });

    it('must not be passed into stream', function(done) {
      var stream = newer({dest: 'collected', extra: 'imported'});

      var paths = ['main'];

      stream.on('data', function(file) {
        assert.notEqual(file.path, path.resolve('imported'));
      });
      stream.on('error', done);
      stream.on('end', done);

      write(stream, paths);
    });

    it('must let other files through stream if an "extra" is newer', function(done) {
      var stream = newer({dest: 'collected', extra: 'imported'});

      var paths = ['main'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest dir that does not exist', function() {

    beforeEach(function() {
      mock({
        source1: 'source1 content',
        source2: 'source2 content',
        source3: 'source3 content'
      });
    });
    afterEach(mock.restore);

    it('passes through all files', function(done) {
      var stream = newer('new/dir');

      var paths = ['source1', 'source2', 'source3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest file that does not exist', function() {

    beforeEach(function() {
      mock({
        file1: 'file1 content',
        file2: 'file2 content',
        file3: 'file3 content',
        dest: {}
      });
    });
    afterEach(mock.restore);

    it('passes through all files', function(done) {
      var stream = newer('dest/concat');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('empty dest dir', function() {

    beforeEach(function() {
      mock({
        source1: 'source1 content',
        source2: 'source2 content',
        source3: 'source3 content',
        dest: {}
      });
    });
    afterEach(mock.restore);

    it('passes through all files', function(done) {
      var stream = newer('dest');

      var paths = ['source1', 'source2', 'source3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest dir with one older file', function() {

    beforeEach(function() {
      mock({
        file1: 'file1 content',
        file2: 'file2 content',
        file3: 'file3 content',
        dest: {
          file2: mock.file({
            content: 'file2 content',
            mtime: new Date(1)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through all files', function(done) {
      var stream = newer('dest');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest dir with one newer file', function() {

    beforeEach(function() {
      mock({
        file1: mock.file({
          content: 'file1 content',
          mtime: new Date(100)
        }),
        file2: mock.file({
          content: 'file2 content',
          mtime: new Date(100)
        }),
        file3: mock.file({
          content: 'file3 content',
          mtime: new Date(100)
        }),
        dest: {
          file2: mock.file({
            content: 'file2 content',
            mtime: new Date(200)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through two newer files', function(done) {
      var stream = newer('dest');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.notEqual(file.path, path.resolve('file2'));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length - 1);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest dir with two newer and one older file', function() {

    beforeEach(function() {
      mock({
        file1: mock.file({
          content: 'file1 content',
          mtime: new Date(100)
        }),
        file2: mock.file({
          content: 'file2 content',
          mtime: new Date(100)
        }),
        file3: mock.file({
          content: 'file3 content',
          mtime: new Date(100)
        }),
        dest: {
          file1: mock.file({
            content: 'file1 content',
            mtime: new Date(150)
          }),
          file2: mock.file({
            content: 'file2 content',
            mtime: new Date(50)
          }),
          file3: mock.file({
            content: 'file3 content',
            mtime: new Date(150)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through one newer file', function(done) {
      var stream = newer('dest');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve('file2'));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, 1);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest file with first source file newer', function() {

    beforeEach(function() {
      mock({
        file1: mock.file({
          content: 'file1 content',
          mtime: new Date(200)
        }),
        file2: mock.file({
          content: 'file2 content',
          mtime: new Date(100)
        }),
        file3: mock.file({
          content: 'file3 content',
          mtime: new Date(100)
        }),
        dest: {
          output: mock.file({
            content: 'file2 content',
            mtime: new Date(150)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through all source files', function(done) {
      var stream = newer('dest/output');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest file with second source file newer', function() {

    beforeEach(function() {
      mock({
        file1: mock.file({
          content: 'file1 content',
          mtime: new Date(100)
        }),
        file2: mock.file({
          content: 'file2 content',
          mtime: new Date(200)
        }),
        file3: mock.file({
          content: 'file3 content',
          mtime: new Date(100)
        }),
        dest: {
          output: mock.file({
            content: 'file2 content',
            mtime: new Date(150)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through all source files', function(done) {
      var stream = newer('dest/output');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest file with last source file newer', function() {

    beforeEach(function() {
      mock({
        file1: mock.file({
          content: 'file1 content',
          mtime: new Date(100)
        }),
        file2: mock.file({
          content: 'file2 content',
          mtime: new Date(100)
        }),
        file3: mock.file({
          content: 'file3 content',
          mtime: new Date(200)
        }),
        dest: {
          output: mock.file({
            content: 'file2 content',
            mtime: new Date(150)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through all source files', function(done) {
      var stream = newer('dest/output');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve(paths[calls]));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, paths.length);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest file with no newer source files', function() {

    beforeEach(function() {
      mock({
        file1: mock.file({
          content: 'file1 content',
          mtime: new Date(100)
        }),
        file2: mock.file({
          content: 'file2 content',
          mtime: new Date(100)
        }),
        file3: mock.file({
          content: 'file3 content',
          mtime: new Date(100)
        }),
        dest: {
          output: mock.file({
            content: 'file2 content',
            mtime: new Date(150)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through no source files', function(done) {
      var stream = newer('dest/output');

      var paths = ['file1', 'file2', 'file3'];

      var calls = 0;
      stream.on('data', function() {
        done(new Error('Expected no source files'));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, 0);
        done();
      });

      write(stream, paths);
    });

  });

  describe('dest file ext and two files', function() {

    beforeEach(function() {
      mock({
        'file1.ext1': mock.file({
          content: 'file1 content',
          mtime: new Date(100)
        }),
        'file2.ext1': mock.file({
          content: 'file2 content',
          mtime: new Date(100)
        }),
        dest: {
          'file1.ext2': mock.file({
            content: 'file1 content',
            mtime: new Date(100)
          }),
          'file2.ext2': mock.file({
            content: 'file2 content',
            mtime: new Date(50)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through one newer file', function(done) {
      var stream = newer({dest: 'dest', ext: '.ext2'});

      var paths = ['file1.ext1', 'file2.ext1'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve('file2.ext1'));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, 1);
        done();
      });

      write(stream, paths);
    });

  });

  describe('custom mapping between source and dest', function() {

    beforeEach(function() {
      mock({
        'file1.ext1': mock.file({
          content: 'file1 content',
          mtime: new Date(100)
        }),
        'file2.ext1': mock.file({
          content: 'file2 content',
          mtime: new Date(100)
        }),
        dest: {
          'file1.ext2': mock.file({
            content: 'file1 content',
            mtime: new Date(100)
          }),
          'file2.ext2': mock.file({
            content: 'file2 content',
            mtime: new Date(50)
          })
        }
      });
    });
    afterEach(mock.restore);

    it('passes through one newer file', function(done) {
      var stream = newer({
        dest: 'dest',
        map: function(destPath) {
          return destPath.replace('.ext1', '.ext2');
        }
      });

      var paths = ['file1.ext1', 'file2.ext1'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve('file2.ext1'));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, 1);
        done();
      });

      write(stream, paths);
    });

    it('allows people to join to dest themselves', function(done) {
      var stream = newer({
        map: function(destPath) {
          return path.join('dest', destPath.replace('.ext1', '.ext2'));
        }
      });

      var paths = ['file1.ext1', 'file2.ext1'];

      var calls = 0;
      stream.on('data', function(file) {
        assert.equal(file.path, path.resolve('file2.ext1'));
        ++calls;
      });

      stream.on('error', done);

      stream.on('end', function() {
        assert.equal(calls, 1);
        done();
      });

      write(stream, paths);
    });

  });

  describe('reports errors', function() {
    beforeEach(function() {
      mock({
        'q': mock.file({
          mtime: new Date(100)
        }),
        dest: {}
      });
    });
    afterEach(mock.restore);

    it('in "data" handlers', function(done) {
      var stream = newer('dest');

      var err = new Error('test');

      stream.on('data', function() {
        throw err;
      });

      stream.on('error', function(caught) {
        assert.equal(caught, err);
        done();
      });

      write(stream, ['q']);
    });

  });

});
