'use strict';

const assume = require('assume');
const { format } = require('logform');
const Writable = require('readable-stream/writable');
const TransportStream = require('../');
const Parent = require('./fixtures/parent');
const { testLevels, testOrder } = require('./fixtures');
const {
  infosFor,
  logFor,
  levelAndMessage,
  toException,
  toWriteReq
} = require('abstract-winston-transport/utils');
const { LEVEL, MESSAGE } = require('triple-beam');

/*
 * Returns the provided `info` object with the appropriate LEVEL,
 * and MESSAGE symbols defined.
 */
function infoify(info) {
  info[LEVEL] = info.level;
  info[MESSAGE] = info.message;
  return info;
};

describe('TransportStream', () => {
  it('should have the appropriate methods defined', () => {
    const transport = new TransportStream();
    assume(transport).instanceof(Writable);
    assume(transport._write).is.a('function');
    // eslint-disable-next-line no-undefined
    assume(transport.log).equals(undefined);
  });

  it('should accept a custom log function invoked on _write', () => {
    const log = logFor(1);
    const transport = new TransportStream({ log });
    assume(transport.log).equals(log);
  });

  it('should invoke a custom log function on _write', done => {
    const info = {
      [LEVEL]: 'test',
      level: 'test',
      message: 'Testing ... 1 2 3.'
    };

    const transport = new TransportStream({
      log(actual) {
        assume(actual).equals(info);
        done();
      }
    });

    transport.write(info);
  });

  describe('_write(info, enc, callback)', () => {
    it('should log to any level when { level: undefined }', done => {
      const expected = testOrder.map(levelAndMessage);
      const transport = new TransportStream({
        log: logFor(testOrder.length, (err, infos) => {
          if (err) {
            return done(err);
          }

          assume(infos.length).equals(expected.length);
          assume(infos).deep.equals(expected);
          done();
        })
      });

      expected.forEach(transport.write.bind(transport));
    });

    it('should not log when no info object is provided', done => {
      const expected = testOrder.map(levelAndMessage).map((info, i) => {
        if (testOrder.length > (i + 1)) {
          info.private = true;
        }

        return info;
      });
      const transport = new TransportStream({
        format: format(info => {
          if (info.private) return false;
          return info;
        })(),
        log: logFor(1, (err, infos) => {
          if (err) {
            return done(err);
          }

          assume(infos.length).equals(1);
          assume(infos.pop()).deep.equals(expected.pop());
          done();
        })
      });

      expected.forEach(transport.write.bind(transport));
    });

    it('should only log messages BELOW the level priority', done => {
      const expected = testOrder.map(levelAndMessage);
      const transport = new TransportStream({
        level: 'info',
        log: logFor(5, (err, infos) => {
          if (err) {
            return done(err);
          }

          assume(infos.length).equals(5);
          assume(infos).deep.equals(expected.slice(0, 5));
          done();
        })
      });

      transport.levels = testLevels;
      expected.forEach(transport.write.bind(transport));
    });

    it('{ level } should be ignored when { handleExceptions: true }', () => {
      const expected = testOrder.map(levelAndMessage).map(info => {
        info.exception = true;
        return info;
      });

      const transport = new TransportStream({
        level: 'info',
        log: logFor(testOrder.length, (err, infos) => {
          // eslint-disable-next-line no-undefined
          assume(err).equals(undefined);
          assume(infos.length).equals(expected.length);
          assume(infos).deep.equals(expected);
        })
      });

      transport.levels = testLevels;
      expected.forEach(transport.write.bind(transport));
    });

    describe('when { exception: true } in info', () => {
      it('should not invoke log when { handleExceptions: false }', done => {
        const expected = [{
          exception: true,
          [LEVEL]: 'error',
          level: 'error',
          message: 'Test exception handling'
        }, {
          [LEVEL]: 'test',
          level: 'test',
          message: 'Testing ... 1 2 3.'
        }];

        const transport = new TransportStream({
          log(info) {
            // eslint-disable-next-line no-undefined
            assume(info.exception).equals(undefined);
            done();
          }
        });

        expected.forEach(transport.write.bind(transport));
      });

      it('should invoke log when { handleExceptions: true }', done => {
        const actual = [];
        const expected = [{
          exception: true, [LEVEL]: 'error',
          level: 'error',
          message: 'Test exception handling'
        }, {
          [LEVEL]: 'test',
          level: 'test',
          message: 'Testing ... 1 2 3.'
        }];

        const transport = new TransportStream({
          handleExceptions: true,
          log(info, next) {
            actual.push(info);
            if (actual.length === expected.length) {
              assume(actual).deep.equals(expected);
              return done();
            }

            next();
          }
        });

        expected.forEach(transport.write.bind(transport));
      });
    });
  });

  describe('_writev(chunks, callback)', () => {
    it('invokes .log() for each of the valid chunks when necessary in streams plumbing', done => {
      const expected = infosFor({
        count: 50,
        levels: testOrder
      });

      const transport = new TransportStream({
        log: logFor(50 * testOrder.length, (err, infos) => {
          if (err) {
            return done(err);
          }

          assume(infos.length).equals(expected.length);
          assume(infos).deep.equals(expected);
          done();
        })
      });

      //
      // Make the standard _write throw to ensure that _writev is called.
      //
      transport._write = () => {
        throw new Error('TransportStream.prototype._write should never be called.');
      };

      transport.cork();
      expected.forEach(transport.write.bind(transport));
      transport.uncork();
    });

    it('should not log when no info object is provided in streams plumbing', done => {
      const expected = testOrder.map(levelAndMessage).map((info, i) => {
        if (testOrder.length > (i + 1)) {
          info.private = true;
        }

        return info;
      });

      const transport = new TransportStream({
        format: format(info => {
          if (info.private) {
            return false;
          }

          return info;
        })(),
        log: logFor(1, (err, infos) => {
          if (err) {
            return done(err);
          }

          assume(infos.length).equals(1);
          assume(infos.pop()).deep.equals(expected.pop());
          done();
        })
      });

      //
      // Make the standard _write throw to ensure that _writev is called.
      //
      transport._write = () => {
        throw new Error('TransportStream.prototype._write should never be called.');
      };

      transport.cork();
      expected.forEach(transport.write.bind(transport));
      transport.uncork();
    });


    it('ensures a format is applied to each info when no .logv is defined', done => {
      const expected = infosFor({ count: 10, levels: testOrder });
      const transport = new TransportStream({
        format: format.json(),
        log: logFor(10 * testOrder.length, (err, infos) => {
          if (err) {
            return done(err);
          }

          assume(infos.length).equals(expected.length);
          infos.forEach((info, i) => {
            assume(info[MESSAGE]).equals(JSON.stringify(expected[i]));
          });

          done();
        })
      });

      //
      // Make the standard _write throw to ensure that _writev is called.
      //
      transport._write = () => {
        throw new Error('TransportStream.prototype._write should never be called.');
      };

      transport.cork();
      expected.forEach(transport.write.bind(transport));
      transport.uncork();
    });

    it('invokes .logv with all valid chunks when necessary in streams plumbing', done => {
      const expected = infosFor({
        count: 50,
        levels: testOrder
      });
      const transport = new TransportStream({
        level: 'info',
        log() {
          throw new Error('.log() should never be called');
        },
        logv(chunks, callback) {
          assume(chunks.length).equals(250);
          callback(); // eslint-disable-line callback-return
          done();
        }
      });

      //
      // Make the standard _write throw to ensure that _writev is called.
      //
      transport._write = () => {
        throw new Error('TransportStream.prototype._write should never be called.');
      };

      transport.cork();
      transport.levels = testLevels;
      expected.forEach(transport.write.bind(transport));
      transport.uncork();
    });
  });

  describe('parent (i.e. "logger") ["pipe", "unpipe"]', () => {
    it('should define { level, levels } on "pipe"', done => {
      const parent = new Parent({
        level: 'info',
        levels: testLevels
      });

      const transport = new TransportStream({
        log(info, next) {
          assume(info.level).equals('info');
          assume(info.message).equals('ok sure');
          next();
          done();
        }
      });

      parent.pipe(transport);
      setImmediate(() => {
        assume(transport.level).equals(undefined);
        assume(transport.levels).equals(testLevels);
        assume(transport.parent).equals(parent);
        assume(transport.parent.level).equals('info');
        transport.write(infoify({ level: 'parrot', message: 'never logged' }));
        transport.write(infoify({ level: 'info', message: 'ok sure' }));
      });
    });

    it('should not overwrite existing { level } on "pipe"', done => {
      const parent = new Parent({
        level: 'info',
        levels: testLevels
      });

      const transport = new TransportStream({
        level: 'error',
        log(info, next) {
          assume(info.level).equals('error');
          assume(info.message).equals('ok sure');
          next();
          done();
        }
      });

      parent.pipe(transport);
      setImmediate(() => {
        assume(transport.level).equals('error');
        assume(transport.levels).equals(testLevels);
        assume(transport.parent).equals(parent);
        transport.write(infoify({ level: 'info', message: 'never logged' }));
        transport.write(infoify({ level: 'error', message: 'ok sure' }));
      });
    });

    it('should respond to changes in parent logging level', done => {
      const parent = new Parent({
        level: 'error',
        levels: testLevels
      });

      const transport = new TransportStream({
        log(info, next) {
          assume(info.level).equals('parrot');
          assume(info.message).equals('eventually log this');
          next();
          done();
        }
      });

      parent.pipe(transport);
      setImmediate(() => {
        assume(transport.levels).equals(testLevels);
        assume(transport.parent).equals(parent);
        transport.write(infoify({ level: 'info', message: 'never logged' }));

        parent.level = 'parrot';
        transport.write(infoify({ level: 'parrot', message: 'eventually log this' }));
      });
    });

    it('should unset parent on "unpipe"', done => {
      const parent = new Parent({
        level: 'info',
        levels: testLevels
      });

      const transport = new TransportStream({
        level: 'error',
        log() {}
      });

      //
      // Trigger "pipe" first so that transport.parent is set.
      //
      parent.pipe(transport);
      setImmediate(() => {
        assume(transport.parent).equals(parent);

        //
        // Now verify that after "unpipe" it is set to 'null'.
        //
        parent.unpipe(transport);
        setImmediate(() => {
          assume(transport.parent).equals(null);
          done();
        });
      });
    });

    it('should invoke a close method on "unpipe"', done => {
      const parent = new Parent({
        level: 'info',
        levels: testLevels
      });

      const transport = new TransportStream({
        log() {}
      });

      //
      // Test will only successfully complete when `close`
      // is invoked
      //
      transport.close = () => {
        assume(transport.parent).equals(null);
        done();
      };

      //
      // Trigger "pipe" first so that transport.parent is set.
      //
      parent.pipe(transport);
      setImmediate(() => {
        assume(transport.parent).equals(parent);
        parent.unpipe(transport);
      });
    });
  });

  describe('_accept(info)', function () {
    it('should filter only log messages BELOW the level priority', () => {
      const expected = testOrder
        .map(levelAndMessage)
        .map(toWriteReq);

      const transport = new TransportStream({
        level: 'info'
      });

      transport.levels = testLevels;
      const filtered = expected.filter(transport._accept, transport)
        .map(write => write.chunk.level);

      assume(filtered).deep.equals([
        'error',
        'warn',
        'dog',
        'cat',
        'info'
      ]);
    });

    it('should filter out { exception: true } when { handleExceptions: false }', () => {
      const expected = testOrder
        .map(toException)
        .map(toWriteReq);

      const transport = new TransportStream({
        handleExceptions: false,
        level: 'info'
      });

      transport.levels = testLevels;
      const filtered = expected.filter(transport._accept, transport)
        .map(info => info.level);

      assume(filtered).deep.equals([]);
    });

    it('should include ALL { exception: true } when { handleExceptions: true }', () => {
      const expected = testOrder
        .map(toException)
        .map(toWriteReq);

      const transport = new TransportStream({
        handleExceptions: true,
        level: 'info'
      });

      transport.levels = testLevels;
      const filtered = expected.filter(transport._accept, transport)
        .map(write => write.chunk.level);

      assume(filtered).deep.equals(testOrder);
    });
  });

  describe('{ format }', function () {
    it('logs the output of the provided format', done => {
      const expected = {
        [LEVEL]: 'info',
        level: 'info',
        message: 'there will be json'
      };

      const transport = new TransportStream({
        format: format.json(),
        log(info) {
          assume(info[MESSAGE]).equals(JSON.stringify(expected));
          done();
        }
      });

      transport.write(expected);
    });

    it('treats the original object immutable', done => {
      const expected = {
        [LEVEL]: 'info',
        level: 'info',
        message: 'there will be json'
      };

      const transport = new TransportStream({
        format: format.json(),
        log(info) {
          assume(info).not.equals(expected);
          done();
        }
      });

      transport.write(expected);
    });

    it('_write continues to write after a format throws', done => {
      const transport = new TransportStream({
        format: format.printf((info) => {
          // Set a trap.
          if (info.message === 'ENDOR') {
            throw new Error('ITS A TRAP!');
          }

          return info.message;
        }),
        log(info, callback) {
          callback();
          assume(info.level).equals('info');
          assume(info.message).equals('safe');
          done();
        }
      });

      try {
        transport.write({ level: 'info', message: 'ENDOR' });
      } catch (ex) {
        assume(ex.message).equals('ITS A TRAP!');
      }

      transport.write({ level: 'info', message: 'safe' });
    });

    it('_writev continues to write after a format throws', done => {
      const transport = new TransportStream({
        format: format.printf((info) => {
          // Set a trap.
          if (info.message === 'ENDOR') {
            throw new Error('ITS A TRAP!');
          }

          return info.message;
        }),
        log(info, callback) {
          assume(info.level).is.a('string');
          assume(info.message).is.a('string');
          callback();

          if (info.message === 'safe') {
            done();
          }
        }
      });

      const infos = infosFor({
        count: 10,
        levels: testOrder
      });

      try {
        transport.cork();
        infos.forEach(info => transport.write(info));
        transport.write({ level: 'info', message: 'ENDOR' });
        transport.uncork();
      } catch (ex) {
        assume(ex.message).equals('ITS A TRAP!');
      }

      transport.write({ level: 'info', message: 'safe' });
    });
  });

  describe('{ silent }', () => {
    const silentTransport = new TransportStream({
      silent: true,
      format: format.json(),
      log() {
        assume(false).true('.log() was called improperly');
      }
    });

    it('{ silent: true } ._write() never calls `.log`', done => {
      const expected = {
        [LEVEL]: 'info',
        level: 'info',
        message: 'there will be json'
      };

      silentTransport.write(expected);
      setImmediate(() => done());
    });

    it('{ silent: true } ._writev() never calls `.log`', done => {
      const expected = {
        [LEVEL]: 'info',
        level: 'info',
        message: 'there will be json'
      };

      silentTransport.cork();
      for (let i = 0; i < 15; i++) {
        silentTransport.write(expected);
      }

      silentTransport.uncork();
      setImmediate(() => done());
    });

    it('{ silent: true } ensures ._accept(write) always returns false', () => {
      const accepted = silentTransport._accept({ chunk: {} });
      assume(accepted).false();
    });
  });
});
