'use strict';
var common = require('../common');
var PassThrough = require('../../lib/_stream_passthrough');
var Transform = require('../../lib/_stream_transform');

/////
module.exports = function (t) {
  t.test('writable side consumption', function(t) {
    var tx = new Transform({
      highWaterMark: 10
    });

    var transformed = 0;
    tx._transform = function(chunk, encoding, cb) {
      transformed += chunk.length;
      tx.push(chunk);
      cb();
    };

    for (var i = 1; i <= 10; i++) {
      tx.write(Buffer.alloc(i));
    }
    tx.end();

    t.equal(tx._readableState.length, 10);
    t.equal(transformed, 10);
    t.equal(tx._transformState.writechunk.length, 5);
    t.same(tx._writableState.getBuffer().map(function(c) {
      return c.chunk.length;
    }), [6, 7, 8, 9, 10]);

    t.end();
  });

  t.test('passthrough', function(t) {
    var pt = new PassThrough();

    pt.write(Buffer.from('foog'));
    pt.write(Buffer.from('bark'));
    pt.write(Buffer.from('bazy'));
    pt.write(Buffer.from('kuel'));
    pt.end();

    t.equal(pt.read(5).toString(), 'foogb');
    t.equal(pt.read(5).toString(), 'arkba');
    t.equal(pt.read(5).toString(), 'zykue');
    t.equal(pt.read(5).toString(), 'l');
    t.end();
  });

  t.test('object passthrough', function(t) {
    var pt = new PassThrough({ objectMode: true });

    pt.write(1);
    pt.write(true);
    pt.write(false);
    pt.write(0);
    pt.write('foo');
    pt.write('');
    pt.write({ a: 'b'});
    pt.end();

    t.equal(pt.read(), 1);
    t.equal(pt.read(), true);
    t.equal(pt.read(), false);
    t.equal(pt.read(), 0);
    t.equal(pt.read(), 'foo');
    t.equal(pt.read(), '');
    t.same(pt.read(), { a: 'b'});
    t.end();
  });

  t.test('simple transform', function(t) {
    var pt = new Transform();
    pt._transform = function(c, e, cb) {
      var ret = Buffer.alloc(c.length);
      ret.fill('x');
      pt.push(ret);
      cb();
    };

    pt.write(Buffer.from('foog'));
    pt.write(Buffer.from('bark'));
    pt.write(Buffer.from('bazy'));
    pt.write(Buffer.from('kuel'));
    pt.end();

    t.equal(pt.read(5).toString(), 'xxxxx');
    t.equal(pt.read(5).toString(), 'xxxxx');
    t.equal(pt.read(5).toString(), 'xxxxx');
    t.equal(pt.read(5).toString(), 'x');
    t.end();
  });

  t.test('simple object transform', function(t) {
    var pt = new Transform({ objectMode: true });
    pt._transform = function(c, e, cb) {
      pt.push(JSON.stringify(c));
      cb();
    };

    pt.write(1);
    pt.write(true);
    pt.write(false);
    pt.write(0);
    pt.write('foo');
    pt.write('');
    pt.write({ a: 'b'});
    pt.end();

    t.equal(pt.read(), '1');
    t.equal(pt.read(), 'true');
    t.equal(pt.read(), 'false');
    t.equal(pt.read(), '0');
    t.equal(pt.read(), '"foo"');
    t.equal(pt.read(), '""');
    t.equal(pt.read(), '{"a":"b"}');
    t.end();
  });

  t.test('async passthrough', function(t) {
    var pt = new Transform();
    pt._transform = function(chunk, encoding, cb) {
      setTimeout(function() {
        pt.push(chunk);
        cb();
      }, 10);
    };

    pt.write(Buffer.from('foog'));
    pt.write(Buffer.from('bark'));
    pt.write(Buffer.from('bazy'));
    pt.write(Buffer.from('kuel'));
    pt.end();

    pt.on('finish', function() {
      t.equal(pt.read(5).toString(), 'foogb');
      t.equal(pt.read(5).toString(), 'arkba');
      t.equal(pt.read(5).toString(), 'zykue');
      t.equal(pt.read(5).toString(), 'l');
      t.end();
    });
  });

  t.test('assymetric transform (expand)', function(t) {
    var pt = new Transform();

    // emit each chunk 2 times.
    pt._transform = function(chunk, encoding, cb) {
      setTimeout(function() {
        pt.push(chunk);
        setTimeout(function() {
          pt.push(chunk);
          cb();
        }, 10);
      }, 10);
    };

    pt.write(Buffer.from('foog'));
    pt.write(Buffer.from('bark'));
    pt.write(Buffer.from('bazy'));
    pt.write(Buffer.from('kuel'));
    pt.end();

    pt.on('finish', function() {
      t.equal(pt.read(5).toString(), 'foogf');
      t.equal(pt.read(5).toString(), 'oogba');
      t.equal(pt.read(5).toString(), 'rkbar');
      t.equal(pt.read(5).toString(), 'kbazy');
      t.equal(pt.read(5).toString(), 'bazyk');
      t.equal(pt.read(5).toString(), 'uelku');
      t.equal(pt.read(5).toString(), 'el');
      t.end();
    });
  });

  t.test('assymetric transform (compress)', function(t) {
    var pt = new Transform();

    // each output is the first char of 3 consecutive chunks,
    // or whatever's left.
    pt.state = '';

    pt._transform = function(chunk, encoding, cb) {
      if (!chunk)
        chunk = '';
      var s = chunk.toString();
      setTimeout(function() {
        this.state += s.charAt(0);
        if (this.state.length === 3) {
          pt.push(Buffer.from(this.state));
          this.state = '';
        }
        cb();
      }.bind(this), 10);
    };

    pt._flush = function(cb) {
      // just output whatever we have.
      pt.push(Buffer.from(this.state));
      this.state = '';
      cb();
    };

    pt.write(Buffer.from('aaaa'));
    pt.write(Buffer.from('bbbb'));
    pt.write(Buffer.from('cccc'));
    pt.write(Buffer.from('dddd'));
    pt.write(Buffer.from('eeee'));
    pt.write(Buffer.from('aaaa'));
    pt.write(Buffer.from('bbbb'));
    pt.write(Buffer.from('cccc'));
    pt.write(Buffer.from('dddd'));
    pt.write(Buffer.from('eeee'));
    pt.write(Buffer.from('aaaa'));
    pt.write(Buffer.from('bbbb'));
    pt.write(Buffer.from('cccc'));
    pt.write(Buffer.from('dddd'));
    pt.end();

    // 'abcdeabcdeabcd'
    pt.on('finish', function() {
      t.equal(pt.read(5).toString(), 'abcde');
      t.equal(pt.read(5).toString(), 'abcde');
      t.equal(pt.read(5).toString(), 'abcd');
      t.end();
    });
  });

  // this tests for a stall when data is written to a full stream
  // that has empty transforms.
  t.test('complex transform', function(t) {
    var count = 0;
    var saved = null;
    var pt = new Transform({highWaterMark:3});
    pt._transform = function(c, e, cb) {
      if (count++ === 1)
        saved = c;
      else {
        if (saved) {
          pt.push(saved);
          saved = null;
        }
        pt.push(c);
      }

      cb();
    };

    pt.once('readable', function() {
      process.nextTick(function() {
        pt.write(Buffer.from('d'));
        pt.write(Buffer.from('ef'), function() {
          pt.end();
          t.end();
        });
        t.equal(pt.read().toString(), 'abcdef');
        t.equal(pt.read(), null);
      });
    });

    pt.write(Buffer.from('abc'));
  });


  t.test('passthrough event emission', function(t) {
    var pt = new PassThrough();
    var emits = 0;
    pt.on('readable', function() {
      var state = pt._readableState;
      //console.error('>>> emit readable %d', emits);
      emits++;
    });

    var i = 0;

    pt.write(Buffer.from('foog'));

    //console.error('need emit 0');
    pt.write(Buffer.from('bark'));

    //console.error('should have emitted readable now 1 === %d', emits);
    t.equal(emits, 1);

    t.equal(pt.read(5).toString(), 'foogb');
    t.equal(pt.read(5) + '', 'null');

    //console.error('need emit 1');

    pt.write(Buffer.from('bazy'));
    //console.error('should have emitted, but not again');
    pt.write(Buffer.from('kuel'));

    //console.error('should have emitted readable now 2 === %d', emits);
    t.equal(emits, 2);

    t.equal(pt.read(5).toString(), 'arkba');
    t.equal(pt.read(5).toString(), 'zykue');
    t.equal(pt.read(5), null);

    //console.error('need emit 2');

    pt.end();

    t.equal(emits, 3);

    t.equal(pt.read(5).toString(), 'l');
    t.equal(pt.read(5), null);

    //console.error('should not have emitted again');
    t.equal(emits, 3);
    t.end();
  });

  t.test('passthrough event emission reordered', function(t) {
    var pt = new PassThrough();
    var emits = 0;
    pt.on('readable', function() {
      //console.error('emit readable', emits);
      emits++;
    });

    pt.write(Buffer.from('foog'));
    //console.error('need emit 0');
    pt.write(Buffer.from('bark'));
    //console.error('should have emitted readable now 1 === %d', emits);
    t.equal(emits, 1);

    t.equal(pt.read(5).toString(), 'foogb');
    t.equal(pt.read(5), null);

    //console.error('need emit 1');
    pt.once('readable', function() {
      t.equal(pt.read(5).toString(), 'arkba');

      t.equal(pt.read(5), null);

      //console.error('need emit 2');
      pt.once('readable', function() {
        t.equal(pt.read(5).toString(), 'zykue');
        t.equal(pt.read(5), null);
        pt.once('readable', function() {
          t.equal(pt.read(5).toString(), 'l');
          t.equal(pt.read(5), null);
          t.equal(emits, 4);
          t.end();
        });
        pt.end();
      });
      pt.write(Buffer.from('kuel'));
    });

    pt.write(Buffer.from('bazy'));
  });

  t.test('passthrough facaded', function(t) {
    //console.error('passthrough facaded');
    var pt = new PassThrough();
    var datas = [];
    pt.on('data', function(chunk) {
      datas.push(chunk.toString());
    });

    pt.on('end', function() {
      t.same(datas, ['foog', 'bark', 'bazy', 'kuel']);
      t.end();
    });

    pt.write(Buffer.from('foog'));
    setTimeout(function() {
      pt.write(Buffer.from('bark'));
      setTimeout(function() {
        pt.write(Buffer.from('bazy'));
        setTimeout(function() {
          pt.write(Buffer.from('kuel'));
          setTimeout(function() {
            pt.end();
          }, 10);
        }, 10);
      }, 10);
    }, 10);
  });

  t.test('object transform (json parse)', function(t) {
    //console.error('json parse stream');
    var jp = new Transform({ objectMode: true });
    jp._transform = function(data, encoding, cb) {
      try {
        jp.push(JSON.parse(data));
        cb();
      } catch (er) {
        cb(er);
      }
    };

    // anything except null/undefined is fine.
    // those are "magic" in the stream API, because they signal EOF.
    var objects = [
      { foo: 'bar' },
      100,
      'string',
      { nested: { things: [ { foo: 'bar' }, 100, 'string' ] } }
    ];

    var ended = false;
    jp.on('end', function() {
      ended = true;
    });

    forEach(objects, function(obj) {
      jp.write(JSON.stringify(obj));
      var res = jp.read();
      t.same(res, obj);
    });

    jp.end();
    // read one more time to get the 'end' event
    jp.read();

    process.nextTick(function() {
      t.ok(ended);
      t.end();
    });
  });

  t.test('object transform (json stringify)', function(t) {
    //console.error('json parse stream');
    var js = new Transform({ objectMode: true });
    js._transform = function(data, encoding, cb) {
      try {
        js.push(JSON.stringify(data));
        cb();
      } catch (er) {
        cb(er);
      }
    };

    // anything except null/undefined is fine.
    // those are "magic" in the stream API, because they signal EOF.
    var objects = [
      { foo: 'bar' },
      100,
      'string',
      { nested: { things: [ { foo: 'bar' }, 100, 'string' ] } }
    ];

    var ended = false;
    js.on('end', function() {
      ended = true;
    });

    forEach(objects, function(obj) {
      js.write(obj);
      var res = js.read();
      t.equal(res, JSON.stringify(obj));
    });

    js.end();
    // read one more time to get the 'end' event
    js.read();

    process.nextTick(function() {
      t.ok(ended);
      t.end();
    });
  });

  function forEach (xs, f) {
    for (var i = 0, l = xs.length; i < l; i++) {
      f(xs[i], i);
    }
  }
};
