/* eslint-disable no-console */
const t = require('../test-lib/test.js');
const assert = require('assert/strict');

const testModule = {
  'test-module': {
    options: {
      alias: 'testModule'
    },
    init() { }
  }
};

describe('structured logging', function () {
  this.timeout(t.timeout);

  let apos;

  after(function () {
    return t.destroy(apos);
  });

  describe('defaults', function () {

    before(async function () {
      apos = await t.create({
        modules: { ...testModule }
      });
    });

    after(async function () {
      await t.destroy(apos);
      apos = null;
    });

    it('should register structured log and module log handlers', function () {
      assert(apos.structuredLog);
      assert(apos.testModule);
      assert.equal(typeof apos.testModule.logDebug, 'function');
      assert.equal(typeof apos.testModule.logInfo, 'function');
      assert.equal(typeof apos.testModule.logWarn, 'function');
      assert.equal(typeof apos.testModule.logError, 'function');
      assert.deepEqual(apos.structuredLog.filters, {
        '*': { severity: [ 'debug', 'info', 'warn', 'error' ] }
      });
    });

    it('should format entries for readability', function () {
      // id spy
      const id = apos.util.generateId;
      apos.util.generateId = () => 'test-id';
      let savedArgs = [];

      // ### DEBUG
      const debug = console.debug;
      console.debug = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logDebug('event-type');
      assert.equal(savedArgs[0], 'test-module: event-type\n');
      assert.equal(
        savedArgs[1],
`{
  "module": "test-module",
  "type": "event-type",
  "severity": "debug"
}`
      );

      // Message as
      savedArgs = [];
      apos.structuredLog.options.messageAs = 'msg';
      apos.testModule.logDebug('event-type', 'some message');
      assert.equal(
        savedArgs[0],
`{
  "msg": "test-module: event-type: some message",
  "module": "test-module",
  "type": "event-type",
  "severity": "debug"
}`
      );
      assert.equal(typeof savedArgs[1], 'undefined');
      delete apos.structuredLog.options.messageAs;

      // ### INFO
      const info = console.info;
      console.info = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logInfo('event-type');
      assert.equal(savedArgs[0], 'test-module: event-type\n');
      assert.equal(
        savedArgs[1],
`{
  "module": "test-module",
  "type": "event-type",
  "severity": "info"
}`
      );

      // ### WARN
      const warn = console.warn;
      console.warn = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logWarn('event-type');
      assert.equal(savedArgs[0], 'test-module: event-type\n');
      assert.equal(
        savedArgs[1],
`{
  "module": "test-module",
  "type": "event-type",
  "severity": "warn"
}`
      );

      // ### ERROR
      const error = console.error;
      console.error = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logError('event-type');
      assert.equal(savedArgs[0], 'test-module: event-type\n');
      assert.equal(
        savedArgs[1],
`{
  "module": "test-module",
  "type": "event-type",
  "severity": "error"
}`
      );

      // With req
      savedArgs = [];
      apos.testModule.logError(
        apos.task.getReq({
          originalUrl: '/module/test',
          path: '/test',
          method: 'GET',
          ip: '1.2.3.4',
          query: { foo: 'bar' }
        }),
        'event-type'
      );
      assert.equal(savedArgs[0], 'test-module: event-type\n');
      assert.equal(
        savedArgs[1],
`{
  "module": "test-module",
  "type": "event-type",
  "severity": "error",
  "url": "/module/test",
  "path": "/test",
  "method": "GET",
  "ip": "1.2.3.4",
  "query": {
    "foo": "bar"
  },
  "requestId": "test-id"
}`
      );

      // With req and message as
      savedArgs = [];
      apos.structuredLog.options.messageAs = 'message';
      apos.testModule.logError(
        apos.task.getReq({
          originalUrl: '/module/test',
          path: '/test',
          method: 'GET',
          ip: '1.2.3.4',
          query: { foo: 'bar' }
        }),
        'event-type'
      );
      assert.equal(savedArgs.length, 1);
      assert.equal(
        savedArgs[0],
`{
  "message": "test-module: event-type",
  "module": "test-module",
  "type": "event-type",
  "severity": "error",
  "url": "/module/test",
  "path": "/test",
  "method": "GET",
  "ip": "1.2.3.4",
  "query": {
    "foo": "bar"
  },
  "requestId": "test-id"
}`
      );
      delete apos.structuredLog.options.messageAs;

      apos.util.generateId = id;
      console.debug = debug;
      console.info = info;
      console.warn = warn;
      console.error = error;
    });

    it('should log formatted entry: logDebug', function () {
      // id spy
      const id = apos.util.generateId;
      apos.util.generateId = () => 'test-id';

      // debug spy
      const debug = apos.util.logger.debug;
      let savedArgs = [];
      apos.util.logger.debug = (...args) => {
        savedArgs = args;
      };

      // Validate
      assert.throws(() => {
        apos.testModule.logDebug();
      }, function (err) {
        assert.equal(err.message, 'Event type must be a string');
        return true;
      });
      assert.throws(() => {
        apos.testModule.logDebug(apos.task.getReq());
      }, function (err) {
        assert.equal(err.message, 'Event type must be a string');
        return true;
      });
      assert.throws(() => {
        apos.testModule.logDebug(null);
      }, function (err) {
        assert.equal(err.message, 'Event type must be a string');
        return true;
      });
      assert.throws(() => {
        apos.testModule.logDebug(1);
      }, function (err) {
        assert.equal(err.message, 'Event type must be a string');
        return true;
      });

      // Format
      apos.testModule.logDebug('event-type');
      assert.equal(savedArgs[0], 'test-module: event-type');
      assert.deepEqual(savedArgs[1], {
        type: 'event-type',
        severity: 'debug',
        module: 'test-module'
      });

      apos.testModule.logDebug('event-type', 'a message');
      assert.equal(savedArgs[0], 'test-module: event-type: a message');
      assert.deepEqual(savedArgs[1], {
        type: 'event-type',
        severity: 'debug',
        module: 'test-module'
      });

      apos.testModule.logDebug('event-type', 'a message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: a message');
      assert.deepEqual(savedArgs[1], {
        type: 'event-type',
        severity: 'debug',
        module: 'test-module',
        foo: 'bar'
      });

      apos.testModule.logDebug('event-type', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type');
      assert.deepEqual(savedArgs[1], {
        type: 'event-type',
        severity: 'debug',
        module: 'test-module',
        foo: 'bar'
      });

      apos.testModule.logDebug(apos.task.getReq({
        originalUrl: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' }
      }), 'event-type');
      assert.equal(savedArgs[0], 'test-module: event-type');
      assert.deepEqual(savedArgs[1], {
        url: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' },
        requestId: 'test-id',
        type: 'event-type',
        severity: 'debug',
        module: 'test-module'
      });

      apos.testModule.logDebug(apos.task.getReq({
        originalUrl: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' }
      }), 'event-type', 'some message');
      assert.equal(savedArgs[0], 'test-module: event-type: some message');
      assert.deepEqual(savedArgs[1], {
        url: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' },
        requestId: 'test-id',
        type: 'event-type',
        severity: 'debug',
        module: 'test-module'
      });

      apos.testModule.logDebug(apos.task.getReq({
        originalUrl: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' }
      }), 'event-type', 'some message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: some message');
      assert.deepEqual(savedArgs[1], {
        url: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' },
        requestId: 'test-id',
        type: 'event-type',
        severity: 'debug',
        module: 'test-module',
        foo: 'bar'
      });

      apos.testModule.logDebug(apos.task.getReq({
        originalUrl: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' }
      }), 'event-type', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type');
      assert.deepEqual(savedArgs[1], {
        url: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' },
        requestId: 'test-id',
        type: 'event-type',
        severity: 'debug',
        module: 'test-module',
        foo: 'bar'
      });

      apos.util.logger.debug = debug;
      apos.util.generateId = id;
    });

    it('should log formatted entry: logInfo', function () {
      // id spy
      const id = apos.util.generateId;
      apos.util.generateId = () => 'test-id';

      // debug spy
      const info = apos.util.logger.info;
      let savedArgs = [];
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };

      // Validate
      assert.throws(() => {
        apos.testModule.logInfo();
      }, function (err) {
        assert.equal(err.message, 'Event type must be a string');
        return true;
      });

      // Format
      apos.testModule.logInfo('event-type', 'a message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: a message');
      assert.deepEqual(savedArgs[1], {
        type: 'event-type',
        severity: 'info',
        module: 'test-module',
        foo: 'bar'
      });

      apos.testModule.logInfo(apos.task.getReq({
        originalUrl: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' }
      }), 'event-type', 'some message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: some message');
      assert.deepEqual(savedArgs[1], {
        url: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' },
        requestId: 'test-id',
        type: 'event-type',
        severity: 'info',
        module: 'test-module',
        foo: 'bar'
      });

      apos.util.logger.info = info;
      apos.util.generateId = id;
    });

    it('should log formatted entry: logWarn', function () {
      // id spy
      const id = apos.util.generateId;
      apos.util.generateId = () => 'test-id';

      // debug spy
      const warn = apos.util.logger.warn;
      let savedArgs = [];
      apos.util.logger.warn = (...args) => {
        savedArgs = args;
      };

      // Validate
      assert.throws(() => {
        apos.testModule.logWarn();
      }, function (err) {
        assert.equal(err.message, 'Event type must be a string');
        return true;
      });

      // Format
      apos.testModule.logWarn('event-type', 'a message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: a message');
      assert.deepEqual(savedArgs[1], {
        type: 'event-type',
        severity: 'warn',
        module: 'test-module',
        foo: 'bar'
      });

      apos.testModule.logWarn(apos.task.getReq({
        originalUrl: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' }
      }), 'event-type', 'some message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: some message');
      assert.deepEqual(savedArgs[1], {
        url: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' },
        requestId: 'test-id',
        type: 'event-type',
        severity: 'warn',
        module: 'test-module',
        foo: 'bar'
      });

      apos.util.logger.warn = warn;
      apos.util.generateId = id;
    });

    it('should log formatted entry: logError', function () {
      // id spy
      const id = apos.util.generateId;
      apos.util.generateId = () => 'test-id';

      // debug spy
      const error = apos.util.logger.error;
      let savedArgs = [];
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };

      // Validate
      assert.throws(() => {
        apos.testModule.logError();
      }, function (err) {
        assert.equal(err.message, 'Event type must be a string');
        return true;
      });

      // Format
      apos.testModule.logError('event-type', 'a message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: a message');
      assert.deepEqual(savedArgs[1], {
        type: 'event-type',
        severity: 'error',
        module: 'test-module',
        foo: 'bar'
      });

      apos.testModule.logError(apos.task.getReq({
        originalUrl: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' }
      }), 'event-type', 'some message', { foo: 'bar' });
      assert.equal(savedArgs[0], 'test-module: event-type: some message');
      assert.deepEqual(savedArgs[1], {
        url: '/module/test',
        path: '/test',
        method: 'GET',
        ip: '1.2.3.4',
        query: { foo: 'bar' },
        requestId: 'test-id',
        type: 'event-type',
        severity: 'error',
        module: 'test-module',
        foo: 'bar'
      });

      apos.util.logger.error = error;
      apos.util.generateId = id;
    });

    it('should filter entries with minimal config', async function () {
      await t.destroy(apos);
      apos = await t.create({
        modules: {
          ...testModule,
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': {
                  severity: [ 'error' ]
                },
                'test-module': {
                  events: [ 'type1' ]
                }
              }
            }
          }
        }
      });

      assert.deepEqual(apos.structuredLog.filters, {
        '*': {
          severity: [ 'error' ]
        },
        'test-module': {
          events: [ 'type1' ]
        }
      });

      let savedArgs = [];
      const debug = apos.util.logger.debug;
      apos.util.logger.debug = (...args) => {
        savedArgs = args;
      };
      const info = apos.util.logger.info;
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };
      const warn = apos.util.logger.warn;
      apos.util.logger.warn = (...args) => {
        savedArgs = args;
      };
      const error = apos.util.logger.error;
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };

      // ### DEBUG
      // No match
      apos.global.logDebug('type2');
      assert.equal(savedArgs.length, 0);

      // No match - type from another module
      savedArgs = [];
      apos.global.logDebug('type1');
      assert.equal(savedArgs.length, 0);

      // Matches the global severity
      savedArgs = [];
      apos.global.logError('type1');
      assert.equal(savedArgs.length, 2);
      savedArgs = [];
      apos.global.logError('type2');
      assert.equal(savedArgs.length, 2);

      // Matches the module type only
      savedArgs = [];
      apos.testModule.logDebug('type1');
      assert.equal(savedArgs.length, 2);

      // Matches the global severity and module type
      savedArgs = [];
      apos.testModule.logError('type1');
      assert.equal(savedArgs.length, 2);

      // Matches the global severity only
      savedArgs = [];
      apos.testModule.logError('type2');
      assert.equal(savedArgs.length, 2);

      // No match
      savedArgs = [];
      apos.testModule.logWarn('type3');
      assert.equal(savedArgs.length, 0);

      apos.util.logger.debug = debug;
      apos.util.logger.info = info;
      apos.util.logger.warn = warn;
      apos.util.logger.error = error;
    });

    it('should match all with wildcard global config', async function () {
      await t.destroy(apos);
      apos = await t.create({
        modules: {
          ...testModule,
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': true
              }
            }
          }
        }
      });

      assert.deepEqual(apos.structuredLog.filters, {
        '*': {
          severity: [ 'debug', 'info', 'warn', 'error' ]
        }
      });

      let savedArgs = [];
      const debug = apos.util.logger.debug;
      apos.util.logger.debug = (...args) => {
        savedArgs = args;
      };
      const info = apos.util.logger.info;
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };
      const warn = apos.util.logger.warn;
      apos.util.logger.warn = (...args) => {
        savedArgs = args;
      };
      const error = apos.util.logger.error;
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };

      // ### DEBUG
      savedArgs = [];
      apos.testModule.logDebug('type1');
      assert.equal(savedArgs.length, 2);
      savedArgs = [];
      apos.testModule.logInfo('type1');
      assert.equal(savedArgs.length, 2);
      apos.testModule.logWarn('type1');
      assert.equal(savedArgs.length, 2);
      apos.testModule.logError('type1');
      assert.equal(savedArgs.length, 2);

      apos.util.logger.debug = debug;
      apos.util.logger.info = info;
      apos.util.logger.warn = warn;
      apos.util.logger.error = error;
    });

    it('should filter entries with wildcard module events config', async function () {
      await t.destroy(apos);
      apos = await t.create({
        modules: {
          ...testModule,
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': {
                  severity: [ 'error' ],
                  events: [ 'type1' ]
                },
                'test-module': {
                  events: '*'
                }
              }
            }
          }
        }
      });

      assert.deepEqual(apos.structuredLog.filters, {
        '*': {
          severity: [ 'error' ],
          events: [ 'type1' ]
        },
        'test-module': {
          events: [ '*' ]
        }
      });

      let savedArgs = [];
      const debug = apos.util.logger.debug;
      apos.util.logger.debug = (...args) => {
        savedArgs = args;
      };
      const info = apos.util.logger.info;
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };
      const warn = apos.util.logger.warn;
      apos.util.logger.warn = (...args) => {
        savedArgs = args;
      };
      const error = apos.util.logger.error;
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };

      // ### DEBUG
      savedArgs = [];
      // Match the global type only
      apos.global.logDebug('type1');
      assert.equal(savedArgs.length, 2);

      savedArgs = [];
      // No match
      apos.global.logDebug('type2');
      assert.equal(savedArgs.length, 0);

      // Always match because of the event type wildcard
      savedArgs = [];
      apos.testModule.logDebug('any-type');
      assert.equal(savedArgs.length, 2);
      savedArgs = [];
      apos.testModule.logInfo('any-type');
      assert.equal(savedArgs.length, 2);
      apos.testModule.logWarn('any-type');
      assert.equal(savedArgs.length, 2);
      apos.testModule.logError('any-type');
      assert.equal(savedArgs.length, 2);

      apos.util.logger.debug = debug;
      apos.util.logger.info = info;
      apos.util.logger.warn = warn;
      apos.util.logger.error = error;
    });

    it('should filter entries with verbose config', async function () {
      await t.destroy(apos);
      apos = await t.create({
        modules: {
          ...testModule,
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': {
                  severity: [ 'error' ],
                  events: [ 'type1' ]
                },
                'test-module': {
                  severity: [ 'debug' ],
                  events: [ 'type2' ]
                }
              }
            }
          }
        }
      });

      assert.deepEqual(apos.structuredLog.filters, {
        '*': {
          severity: [ 'error' ],
          events: [ 'type1' ]
        },
        'test-module': {
          severity: [ 'debug' ],
          events: [ 'type2' ]
        }
      });

      let savedArgs = [];
      const debug = apos.util.logger.debug;
      apos.util.logger.debug = (...args) => {
        savedArgs = args;
      };
      const info = apos.util.logger.info;
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };
      const warn = apos.util.logger.warn;
      apos.util.logger.warn = (...args) => {
        savedArgs = args;
      };
      const error = apos.util.logger.error;
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };

      // ### DEBUG
      // No match
      apos.global.logDebug('type2');
      assert.equal(savedArgs.length, 0);

      // Matches the type only
      savedArgs = [];
      apos.global.logDebug('type1');
      assert.equal(savedArgs.length, 2);

      // Matches the type and severity
      savedArgs = [];
      apos.global.logError('type1');
      assert.equal(savedArgs.length, 2);

      // Matches the global type and module severity
      savedArgs = [];
      apos.testModule.logDebug('type1');
      assert.equal(savedArgs.length, 2);

      // Matches the global type and severity
      savedArgs = [];
      apos.testModule.logError('type1');
      assert.equal(savedArgs.length, 2);

      // Matches the global type only
      savedArgs = [];
      apos.testModule.logInfo('type1');
      assert.equal(savedArgs.length, 2);

      // Matches the module type only
      savedArgs = [];
      apos.testModule.logInfo('type2');
      assert.equal(savedArgs.length, 2);

      // No match
      savedArgs = [];
      apos.testModule.logWarn('type3');
      assert.equal(savedArgs.length, 0);

      apos.util.logger.debug = debug;
      apos.util.logger.info = info;
      apos.util.logger.warn = warn;
      apos.util.logger.error = error;
    });

    it('it should shutdown logger', async function () {
      await t.destroy(apos);
      apos = await t.create({});

      let called = false;
      apos.util.logger.destroy = async () => {
        called = true;
      };
      await t.destroy(apos);
      apos = null;

      assert.equal(called, true);
    });
  });

  describe('production', function () {
    before(async function () {
      process.env.NODE_ENV = 'production';
      apos = await t.create({
        modules: { ...testModule }
      });
    });

    after(async function () {
      delete process.env.NODE_ENV;
      await t.destroy(apos);
      apos = null;
    });

    it('should set filter configuration', function () {
      assert.deepEqual(apos.structuredLog.filters, {
        '*': { severity: [ 'warn', 'error' ] }
      });
    });

    it('should filter and format entries', function () {
      // id spy
      const id = apos.util.generateId;
      apos.util.generateId = () => 'test-id';
      let savedArgs = [];

      // ### DEBUG
      const debug = console.debug;
      console.debug = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logDebug('event-type');
      assert.equal(typeof savedArgs[0], 'undefined');
      assert.equal(typeof savedArgs[1], 'undefined');

      // Message as
      savedArgs = [];
      apos.structuredLog.options.messageAs = 'msg';
      apos.testModule.logDebug('event-type', 'some message');
      assert.equal(typeof savedArgs[0], 'undefined');
      assert.equal(typeof savedArgs[1], 'undefined');
      delete apos.structuredLog.options.messageAs;

      // ### INFO
      const info = console.info;
      console.info = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logInfo('event-type');
      assert.equal(typeof savedArgs[0], 'undefined');
      assert.equal(typeof savedArgs[1], 'undefined');

      // ### WARN
      const warn = console.warn;
      console.warn = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logWarn('event-type');
      assert.equal(savedArgs[0], 'test-module: event-type');
      assert.equal(
        savedArgs[1],
        '{"module":"test-module","type":"event-type","severity":"warn"}'
      );

      // ### ERROR
      const error = console.error;
      console.error = (...args) => {
        savedArgs = args;
      };

      // Validate formatting
      savedArgs = [];
      apos.testModule.logError('event-type');
      assert.equal(savedArgs[0], 'test-module: event-type');
      assert.equal(
        savedArgs[1],
        '{"module":"test-module","type":"event-type","severity":"error"}'
      );

      apos.util.generateId = id;
      console.debug = debug;
      console.info = info;
      console.warn = warn;
      console.error = error;
    });

    it('should override default filter configuration', async function () {
      await t.destroy(apos);
      apos = await t.create({
        modules: {
          ...testModule,
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': { severity: [ 'info', 'warn', 'error' ] }
              }
            }
          }
        }
      });

      assert.deepEqual(apos.structuredLog.filters, {
        '*': { severity: [ 'info', 'warn', 'error' ] }
      });
    });
  });

  describe('APOS_FILTER_LOGS', function () {
    beforeEach(async function () {
      await t.destroy(apos);
      apos = null;
    });

    after(async function () {
      delete process.env.APOS_FILTER_LOGS;
      await t.destroy(apos);
      apos = null;
    });

    it('should override default filter configuration (wildcard)', async function () {
      process.env.APOS_FILTER_LOGS = '*';
      apos = await t.create({
        modules: {
          ...testModule,
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': { severity: [ 'info', 'warn', 'error' ] },
                'test-module': { events: [ 'type1' ] }
              }
            }
          }
        }
      });

      assert.deepEqual(apos.structuredLog.filters, {
        '*': {
          severity: [ 'debug', 'info', 'warn', 'error' ]
        }
      });
    });

    it('should override filter configuration via env', async function () {
      process.env.APOS_FILTER_LOGS = '*:severity:warn,error;test-module:events:type1,type2:severity:info';
      apos = await t.create({
        modules: {
          ...testModule,
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': { severity: [ 'info', 'warn', 'error' ] },
                'test-module': { events: [ 'type3' ] }
              }
            }
          }
        }
      });

      assert.deepEqual(apos.structuredLog.filters, {
        '*': {
          severity: [ 'warn', 'error' ]
        },
        'test-module': {
          severity: [ 'info' ],
          events: [ 'type1', 'type2' ]
        }
      });
    });
  });

  describe('legacy logging with :messageAs"', function () {
    before(async function () {
      await t.destroy(apos);
      apos = await t.create({
        modules: {
          '@apostrophecms/log': {
            options: {
              messageAs: 'msg'
            }
          }
        }
      });
    });

    after(async function () {
      delete process.env.APOS_FILTER_LOGS;
      await t.destroy(apos);
      apos = null;
    });

    it('should log object: debug', function () {
      let savedArgs = [];
      const saved = apos.util.logger.debug;
      apos.util.logger.debug = (...args) => {
        savedArgs = args;
      };

      savedArgs = [];
      apos.util.debug('some message');
      assert.deepEqual(savedArgs, [ { msg: 'some message' } ]);

      savedArgs = [];
      apos.util.debug({ foo: 'bar' });
      assert.deepEqual(savedArgs, [ { foo: 'bar' } ]);

      savedArgs = [];
      apos.util.debug('some message', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message'
      } ]);

      savedArgs = [];
      apos.util.debug('some message', 'more', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      savedArgs = [];
      apos.util.debug({ foo: 'bar' }, 'some message', 'more');
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      apos.util.logger.debug = saved;
    });

    it('should log object: log', function () {
      let savedArgs = [];
      const saved = apos.util.logger.log;
      apos.util.logger.log = (...args) => {
        savedArgs = args;
      };

      savedArgs = [];
      apos.util.log('some message');
      assert.deepEqual(savedArgs, [ { msg: 'some message' } ]);

      savedArgs = [];
      apos.util.log({ foo: 'bar' });
      assert.deepEqual(savedArgs, [ { foo: 'bar' } ]);

      savedArgs = [];
      apos.util.log('some message', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message'
      } ]);

      savedArgs = [];
      apos.util.log('some message', 'more', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      savedArgs = [];
      apos.util.log({ foo: 'bar' }, 'some message', 'more');
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      apos.util.logger.log = saved;
    });

    it('should log object: info', function () {
      let savedArgs = [];
      const saved = apos.util.logger.info;
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };

      savedArgs = [];
      apos.util.info('some message');
      assert.deepEqual(savedArgs, [ { msg: 'some message' } ]);

      savedArgs = [];
      apos.util.info({ foo: 'bar' });
      assert.deepEqual(savedArgs, [ { foo: 'bar' } ]);

      savedArgs = [];
      apos.util.info('some message', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message'
      } ]);

      savedArgs = [];
      apos.util.info('some message', 'more', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      savedArgs = [];
      apos.util.info({ foo: 'bar' }, 'some message', 'more');
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      apos.util.logger.info = saved;
    });

    it('should log object: warn', function () {
      let savedArgs = [];
      const saved = apos.util.logger.warn;
      apos.util.logger.warn = (...args) => {
        savedArgs = args;
      };

      savedArgs = [];
      apos.util.warn('some message');
      assert.deepEqual(savedArgs, [ { msg: 'some message' } ]);

      savedArgs = [];
      apos.util.warn({ foo: 'bar' });
      assert.deepEqual(savedArgs, [ { foo: 'bar' } ]);

      savedArgs = [];
      apos.util.warn('some message', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message'
      } ]);

      savedArgs = [];
      apos.util.warn('some message', 'more', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      savedArgs = [];
      apos.util.warn({ foo: 'bar' }, 'some message', 'more');
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      apos.util.logger.warn = saved;
    });

    it('should log object: error', function () {
      let savedArgs = [];
      const saved = apos.util.logger.error;
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };

      savedArgs = [];
      apos.util.error('some message');
      assert.deepEqual(savedArgs, [ { msg: 'some message' } ]);

      savedArgs = [];
      apos.util.error({ foo: 'bar' });
      assert.deepEqual(savedArgs, [ { foo: 'bar' } ]);

      savedArgs = [];
      apos.util.error('some message', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message'
      } ]);

      savedArgs = [];
      apos.util.error('some message', 'more', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      savedArgs = [];
      apos.util.error({ foo: 'bar' }, 'some message', 'more');
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      apos.util.logger.error = saved;
    });

    it('should log object: warnDev', function () {
      let savedArgs = [];
      const saved = apos.util.logger.warn;
      apos.util.logger.warn = (...args) => {
        savedArgs = args;
      };

      savedArgs = [];
      apos.util.warnDev('some message');
      assert.deepEqual(savedArgs, [ {
        msg: '⚠️  some message'
      } ]);

      savedArgs = [];
      apos.util.warnDev({ foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar'
      }
      ]);

      savedArgs = [];
      apos.util.warnDev('some message', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: '⚠️  some message'
      } ]);

      savedArgs = [];
      apos.util.warnDev('some message', 'more', { foo: 'bar' });
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: '⚠️  some message',
        args: [ 'more' ]
      } ]);

      savedArgs = [];
      apos.util.warnDev({ foo: 'bar' }, 'some message', 'more');
      assert.deepEqual(savedArgs, [ {
        foo: 'bar',
        msg: 'some message',
        args: [ 'more' ]
      } ]);

      apos.util.logger.warn = saved;
    });
  });

  describe('route error', function () {
    let user;
    let jar;
    let aposError;
    let consoleError;
    let generateId;

    async function login() {
      // Create user and initialize session.
      if (!user) {
        user = await t.createAdmin(apos, {
          username: 'admin',
          password: 'admin'
        });
      }
      jar = await t.getUserJar(apos, user);
      await apos.http.get('/', { jar });
    }

    before(async function () {
      await t.destroy(apos);
      apos = await t.create({
        modules: {
          'test-piece': {
            extend: '@apostrophecms/piece-type',
            fields: {
              add: {
                field1: {
                  type: 'string',
                  label: 'Field1',
                  required: true
                },
                field2: {
                  type: 'string',
                  label: 'Field2',
                  required: true
                }
              }
            }
          },
          'test-module': {
            apiRoutes(self) {
              return {
                post: {
                  async conflict(req) {
                    const err = self.apos.error(
                      'conflict',
                      'Conflict error',
                      { some: 'data' }
                    );
                    err.path = 'some.field';
                    throw err;
                  }
                }
              };
            }
          }
        }
      });
      await login();
      aposError = apos.util.logger.error;
      generateId = apos.util.generateId;
      consoleError = console.error;
    });

    beforeEach(async function () {
      apos.util.logger.error = aposError;
      apos.util.generateId = generateId;
      console.error = consoleError;
    });

    after(async function () {
      await t.destroy(apos);
      apos = null;
    });

    it('should log invalid error', async function () {
      apos.util.generateId = () => 'test-id';
      let savedArgs = [];
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };
      try {
        await apos.http.post('/api/v1/test-piece', {
          body: {},
          jar
        });
      } catch (e) {
        //
      }
      assert.equal(savedArgs[0], 'test-piece: api-error-invalid: invalid');
      assert.equal(savedArgs[1].module, 'test-piece');
      assert.equal(savedArgs[1].type, 'api-error-invalid');
      assert.equal(savedArgs[1].severity, 'error');
      assert.equal(savedArgs[1].url, '/api/v1/test-piece');
      assert.equal(savedArgs[1].path, '/api/v1/test-piece');
      assert.equal(savedArgs[1].method, 'POST');
      assert(savedArgs[1].ip);
      assert.deepEqual(savedArgs[1].query, {});
      assert.equal(savedArgs[1].requestId, 'test-id');
      assert.equal(savedArgs[1].name, 'invalid');
      assert.equal(Array.isArray(savedArgs[1].stack), true);
      assert.equal(savedArgs[1].errorPath, undefined);
      assert.deepEqual(savedArgs[1].data.errors, [
        {
          name: 'required',
          code: 422,
          message: 'required',
          data: {},
          path: 'title'
        },
        {
          name: 'required',
          code: 422,
          message: 'required',
          data: {},
          path: 'field1'
        },
        {
          name: 'required',
          code: 422,
          message: 'required',
          data: {},
          path: 'field2'
        }
      ]);

      // Test the property order
      savedArgs = [];
      apos.util.logger.error = aposError;
      console.error = (...args) => {
        savedArgs = args;
      };

      try {
        await apos.http.post('/api/v1/test-piece', {
          body: {},
          jar
        });
      } catch (e) {
        //
      }
      // Skip IP as it might get changed in CI
      assert.equal(savedArgs[0], 'test-piece: api-error-invalid: invalid\n');
      assert.equal(
        savedArgs[1].startsWith(
`{
  "module": "test-piece",
  "type": "api-error-invalid",
  "severity": "error",
  "url": "/api/v1/test-piece",
  "path": "/api/v1/test-piece",
  "method": "POST",
`
        ),
        true
      );
      assert.equal(
        savedArgs[1].includes(
`
  "query": {},
  "requestId": "test-id",
  "name": "invalid",
  "status": 400,
  "stack": [
`
        ),
        true
      );
      assert.equal(
        savedArgs[1].endsWith(
`
  ],
  "data": {
    "errors": [
      {
        "name": "required",
        "code": 422,
        "message": "required",
        "data": {},
        "path": "title"
      },
      {
        "name": "required",
        "code": 422,
        "message": "required",
        "data": {},
        "path": "field1"
      },
      {
        "name": "required",
        "code": 422,
        "message": "required",
        "data": {},
        "path": "field2"
      }
    ]
  }
}`
        ),
        true
      );

    });

    it('should log conflict error with data and custom message', async function () {
      apos.util.generateId = () => 'test-id';
      let savedArgs = [];
      apos.util.logger.error = (...args) => {
        savedArgs = args;
      };
      try {
        await apos.http.post('/api/v1/test-module/conflict', {
          qs: { foo: 'bar' },
          jar
        });
      } catch (e) {
        //
      }
      assert.equal(savedArgs[0], 'test-module: api-error-conflict: Conflict error');
      assert.equal(savedArgs[1].module, 'test-module');
      assert.equal(savedArgs[1].type, 'api-error-conflict');
      assert.equal(savedArgs[1].severity, 'error');
      assert.equal(savedArgs[1].url, '/api/v1/test-module/conflict?foo=bar');
      assert.equal(savedArgs[1].path, '/api/v1/test-module/conflict');
      assert.equal(savedArgs[1].method, 'POST');
      assert(savedArgs[1].ip);
      assert.deepEqual(savedArgs[1].query, { foo: 'bar' });
      assert.equal(savedArgs[1].requestId, 'test-id');
      assert.equal(savedArgs[1].name, 'conflict');
      assert.equal(Array.isArray(savedArgs[1].stack), true);
      assert.equal(savedArgs[1].errorPath, 'some.field');
      assert.deepEqual(savedArgs[1].data, { some: 'data' });
    });
  });

  describe('login', function () {
    let user;
    let aposInfo;

    async function createInstance() {
      apos = await t.create({
        modules: {
          '@apostrophecms/log': {
            options: {
              filter: {
                '*': {
                  severity: [ 'info' ]
                },
                '@apostrophecms/login': {
                  events: [
                    'incorrect-username',
                    'incorrect-password',
                    'correct-password',
                    'complete'
                  ]
                }
              }
            }
          }
        }
      });
      aposInfo = apos.util.logger.info;
      user = await t.createAdmin(apos, {
        username: 'admin',
        password: 'admin'
      });
      user.password = 'admin';
    }

    before(async function () {
      await t.destroy(apos);
      await createInstance();
    });

    beforeEach(async function () {
      apos.util.logger.info = aposInfo;
    });

    after(async function () {
      await t.destroy(apos);
      apos = null;
    });

    it('should log incorrect username', async function () {
      let savedArgs = [];
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };
      const jar = apos.http.jar();
      try {
        await apos.http.post('/api/v1/@apostrophecms/login/login', {
          body: {
            username: 'incorrect',
            password: user.password,
            session: true
          },
          jar
        });
      } catch (e) {
        //
      }

      assert.equal(savedArgs[0], '@apostrophecms/login: incorrect-username');
      assert(savedArgs[1].ip);
      assert(savedArgs[1].requestId);
      delete savedArgs[1].ip;
      delete savedArgs[1].requestId;
      assert.deepEqual(savedArgs[1], {
        module: '@apostrophecms/login',
        type: 'incorrect-username',
        severity: 'info',
        username: 'incorrect',
        attempts: 1
      });

      savedArgs = [];
      try {
        await apos.http.post('/api/v1/@apostrophecms/login/login', {
          body: {
            username: 'incorrect',
            password: user.password,
            session: true
          },
          jar
        });
      } catch (e) {
        //
      }
      assert.equal(savedArgs[1].attempts, 2);
    });

    it('should log incorrect password', async function () {
      await t.destroy(apos);
      await createInstance();
      let savedArgs = [];
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };
      const jar = apos.http.jar();
      try {
        await apos.http.post('/api/v1/@apostrophecms/login/login', {
          body: {
            username: user.password,
            password: 'incorrect',
            session: true
          },
          jar
        });
      } catch (e) {
        //
      }
      assert.equal(savedArgs[0], '@apostrophecms/login: incorrect-password');
      assert(savedArgs[1].ip);
      assert(savedArgs[1].requestId);
      delete savedArgs[1].ip;
      delete savedArgs[1].requestId;
      assert.deepEqual(savedArgs[1], {
        module: '@apostrophecms/login',
        type: 'incorrect-password',
        severity: 'info',
        username: user.username,
        attempts: 1
      });

      savedArgs = [];
      try {
        await apos.http.post('/api/v1/@apostrophecms/login/login', {
          body: {
            username: user.password,
            password: 'incorrect',
            session: true
          },
          jar
        });
      } catch (e) {
        //
      }
      assert.equal(savedArgs[1].attempts, 2);
    });

    it('should log login complete', async function () {
      await t.destroy(apos);
      await createInstance();

      const jar = apos.http.jar();
      try {
        await apos.http.post('/api/v1/@apostrophecms/login/login', {
          body: {
            username: user.password,
            password: 'incorrect',
            session: true
          },
          jar
        });
      } catch (e) {
        //
      }

      let savedArgs = [];
      apos.util.logger.info = (...args) => {
        savedArgs = args;
      };
      savedArgs = [];
      try {
        await apos.http.post('/api/v1/@apostrophecms/login/login', {
          body: {
            username: user.password,
            password: user.password,
            session: true
          },
          jar
        });
      } catch (e) {
        //
      }
      assert.equal(savedArgs[0], '@apostrophecms/login: complete');
      assert(savedArgs[1].ip);
      assert(savedArgs[1].requestId);
      delete savedArgs[1].ip;
      delete savedArgs[1].requestId;
      assert.deepEqual(savedArgs[1], {
        module: '@apostrophecms/login',
        type: 'complete',
        severity: 'info',
        url: '/api/v1/@apostrophecms/login/login',
        path: '/api/v1/@apostrophecms/login/login',
        method: 'POST',
        query: {},
        username: 'admin',
        attempts: 1
      });
    });
  });
});
